├── .github └── workflows │ └── main.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .pre-commit-hooks.yaml ├── LICENSE ├── README.md ├── pyupgrade ├── __init__.py ├── __main__.py ├── _ast_helpers.py ├── _data.py ├── _main.py ├── _plugins │ ├── __init__.py │ ├── collections_abc.py │ ├── constant_fold.py │ ├── datetime_utc_alias.py │ ├── default_encoding.py │ ├── defaultdict_lambda.py │ ├── dict_literals.py │ ├── exceptions.py │ ├── format_locals.py │ ├── fstrings.py │ ├── identity_equality.py │ ├── imports.py │ ├── io_open.py │ ├── legacy.py │ ├── lru_cache.py │ ├── metaclass_type.py │ ├── mock.py │ ├── native_literals.py │ ├── new_style_classes.py │ ├── open_mode.py │ ├── percent_format.py │ ├── set_literals.py │ ├── shlex_join.py │ ├── six_base_classes.py │ ├── six_calls.py │ ├── six_metaclasses.py │ ├── six_remove_decorators.py │ ├── six_simple.py │ ├── subprocess_run.py │ ├── type_of_primitive.py │ ├── typing_classes.py │ ├── typing_pep563.py │ ├── typing_pep585.py │ ├── typing_pep604.py │ ├── typing_pep646_unpack.py │ ├── typing_pep696_typevar_defaults.py │ ├── typing_text.py │ ├── unittest_aliases.py │ ├── unpack_list_comprehension.py │ └── versioned_branches.py ├── _string_helpers.py └── _token_helpers.py ├── requirements-dev.txt ├── setup.cfg ├── setup.py ├── testing └── generate-imports ├── tests ├── __init__.py ├── _plugins │ ├── __init__.py │ └── typing_pep604_test.py ├── features │ ├── __init__.py │ ├── binary_literals_test.py │ ├── capture_output_test.py │ ├── collections_abc_test.py │ ├── constant_fold_test.py │ ├── datetime_utc_alias_test.py │ ├── default_encoding_test.py │ ├── defaultdict_lambda_test.py │ ├── dict_literals_test.py │ ├── encoding_cookie_test.py │ ├── escape_sequences_test.py │ ├── exceptions_test.py │ ├── extra_parens_test.py │ ├── format_literals_test.py │ ├── format_locals_test.py │ ├── fstrings_test.py │ ├── identity_equality_test.py │ ├── import_removals_test.py │ ├── import_replaces_test.py │ ├── io_open_test.py │ ├── lru_cache_test.py │ ├── metaclass_type_test.py │ ├── mock_test.py │ ├── native_literals_test.py │ ├── new_style_classes_test.py │ ├── open_mode_test.py │ ├── percent_format_test.py │ ├── set_literals_test.py │ ├── shlex_join_test.py │ ├── six_b_test.py │ ├── six_remove_decorators_test.py │ ├── six_simple_test.py │ ├── six_test.py │ ├── super_test.py │ ├── type_of_primitive_test.py │ ├── typing_classes_test.py │ ├── typing_pep563_test.py │ ├── typing_pep585_test.py │ ├── typing_pep604_test.py │ ├── typing_pep646_unpack_test.py │ ├── typing_pep696_typevar_defaults_test.py │ ├── typing_text_test.py │ ├── unicode_literals_test.py │ ├── unittest_aliases_test.py │ ├── universal_newlines_to_text_test.py │ ├── unpack_list_comprehension_test.py │ ├── versioned_branches_test.py │ └── yield_from_test.py ├── main_test.py └── string_helpers_test.py └── tox.ini /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | 3 | on: 4 | push: 5 | branches: [main, test-me-*] 6 | tags: '*' 7 | pull_request: 8 | 9 | jobs: 10 | main-windows: 11 | uses: asottile/workflows/.github/workflows/tox.yml@v1.8.1 12 | with: 13 | env: '["py39"]' 14 | os: windows-latest 15 | main-linux: 16 | uses: asottile/workflows/.github/workflows/tox.yml@v1.8.1 17 | with: 18 | env: '["py39", "py310", "py311", "py312"]' 19 | os: ubuntu-latest 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info 2 | *.pyc 3 | /.coverage 4 | /.tox 5 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: trailing-whitespace 6 | - id: end-of-file-fixer 7 | - id: check-yaml 8 | - id: debug-statements 9 | - id: double-quote-string-fixer 10 | - id: name-tests-test 11 | - id: requirements-txt-fixer 12 | - repo: https://github.com/asottile/setup-cfg-fmt 13 | rev: v2.8.0 14 | hooks: 15 | - id: setup-cfg-fmt 16 | - repo: https://github.com/asottile/reorder-python-imports 17 | rev: v3.15.0 18 | hooks: 19 | - id: reorder-python-imports 20 | args: [--py39-plus, --add-import, 'from __future__ import annotations'] 21 | - repo: https://github.com/asottile/add-trailing-comma 22 | rev: v3.2.0 23 | hooks: 24 | - id: add-trailing-comma 25 | - repo: https://github.com/asottile/pyupgrade 26 | rev: v3.20.0 27 | hooks: 28 | - id: pyupgrade 29 | args: [--py39-plus] 30 | - repo: https://github.com/hhatto/autopep8 31 | rev: v2.3.2 32 | hooks: 33 | - id: autopep8 34 | - repo: https://github.com/PyCQA/flake8 35 | rev: 7.2.0 36 | hooks: 37 | - id: flake8 38 | - repo: https://github.com/pre-commit/mirrors-mypy 39 | rev: v1.16.0 40 | hooks: 41 | - id: mypy 42 | -------------------------------------------------------------------------------- /.pre-commit-hooks.yaml: -------------------------------------------------------------------------------- 1 | - id: pyupgrade 2 | name: pyupgrade 3 | description: Automatically upgrade syntax for newer versions. 4 | entry: pyupgrade 5 | language: python 6 | types: [python] 7 | # for backward compatibility 8 | files: '' 9 | minimum_pre_commit_version: 0.15.0 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Anthony Sottile 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /pyupgrade/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asottile/pyupgrade/1a0b8a1996416e51e374431887b12aae3f8d3e80/pyupgrade/__init__.py -------------------------------------------------------------------------------- /pyupgrade/__main__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pyupgrade._main import main 4 | 5 | if __name__ == '__main__': 6 | raise SystemExit(main()) 7 | -------------------------------------------------------------------------------- /pyupgrade/_ast_helpers.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import ast 4 | import warnings 5 | from collections.abc import Container 6 | 7 | from tokenize_rt import Offset 8 | 9 | 10 | def ast_parse(contents_text: str) -> ast.Module: 11 | # intentionally ignore warnings, we might be fixing warning-ridden syntax 12 | with warnings.catch_warnings(): 13 | warnings.simplefilter('ignore') 14 | return ast.parse(contents_text.encode()) 15 | 16 | 17 | def ast_to_offset(node: ast.expr | ast.stmt) -> Offset: 18 | return Offset(node.lineno, node.col_offset) 19 | 20 | 21 | def is_name_attr( 22 | node: ast.AST, 23 | imports: dict[str, set[str]], 24 | mods: tuple[str, ...], 25 | names: Container[str], 26 | ) -> bool: 27 | return ( 28 | isinstance(node, ast.Name) and 29 | node.id in names and 30 | any(node.id in imports[mod] for mod in mods) 31 | ) or ( 32 | isinstance(node, ast.Attribute) and 33 | isinstance(node.value, ast.Name) and 34 | node.value.id in mods and 35 | node.attr in names 36 | ) 37 | 38 | 39 | def has_starargs(call: ast.Call) -> bool: 40 | return ( 41 | any(k.arg is None for k in call.keywords) or 42 | any(isinstance(a, ast.Starred) for a in call.args) 43 | ) 44 | 45 | 46 | def contains_await(node: ast.AST) -> bool: 47 | for node_ in ast.walk(node): 48 | if isinstance(node_, ast.Await): 49 | return True 50 | else: 51 | return False 52 | 53 | 54 | def is_async_listcomp(node: ast.ListComp) -> bool: 55 | return ( 56 | any(gen.is_async for gen in node.generators) or 57 | contains_await(node) 58 | ) 59 | 60 | 61 | def is_type_check(node: ast.AST) -> bool: 62 | return ( 63 | isinstance(node, ast.Call) and 64 | isinstance(node.func, ast.Name) and 65 | node.func.id in {'isinstance', 'issubclass'} and 66 | len(node.args) == 2 and 67 | not has_starargs(node) 68 | ) 69 | -------------------------------------------------------------------------------- /pyupgrade/_data.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import ast 4 | import collections 5 | import pkgutil 6 | from collections.abc import Iterable 7 | from typing import Callable 8 | from typing import NamedTuple 9 | from typing import Protocol 10 | from typing import TypeVar 11 | 12 | from tokenize_rt import Offset 13 | from tokenize_rt import Token 14 | 15 | from pyupgrade import _plugins 16 | 17 | Version = tuple[int, ...] 18 | 19 | 20 | class Settings(NamedTuple): 21 | min_version: Version = (3,) 22 | keep_percent_format: bool = False 23 | keep_mock: bool = False 24 | keep_runtime_typing: bool = False 25 | 26 | 27 | class State(NamedTuple): 28 | settings: Settings 29 | from_imports: dict[str, set[str]] 30 | in_annotation: bool = False 31 | 32 | 33 | AST_T = TypeVar('AST_T', bound=ast.AST) 34 | TokenFunc = Callable[[int, list[Token]], None] 35 | ASTFunc = Callable[[State, AST_T, ast.AST], Iterable[tuple[Offset, TokenFunc]]] 36 | 37 | RECORD_FROM_IMPORTS = frozenset(( 38 | '__future__', 39 | 'asyncio', 40 | 'collections', 41 | 'collections.abc', 42 | 'functools', 43 | 'mmap', 44 | 'os', 45 | 'select', 46 | 'six', 47 | 'six.moves', 48 | 'socket', 49 | 'subprocess', 50 | 'sys', 51 | 'typing', 52 | 'typing_extensions', 53 | )) 54 | 55 | FUNCS: ASTCallbackMapping # python/mypy#17566 56 | FUNCS = collections.defaultdict(list) # type: ignore[assignment] 57 | 58 | 59 | def register(tp: type[AST_T]) -> Callable[[ASTFunc[AST_T]], ASTFunc[AST_T]]: 60 | def register_decorator(func: ASTFunc[AST_T]) -> ASTFunc[AST_T]: 61 | FUNCS[tp].append(func) 62 | return func 63 | return register_decorator 64 | 65 | 66 | class ASTCallbackMapping(Protocol): 67 | def __getitem__(self, tp: type[AST_T]) -> list[ASTFunc[AST_T]]: ... 68 | 69 | 70 | def visit( 71 | funcs: ASTCallbackMapping, 72 | tree: ast.Module, 73 | settings: Settings, 74 | ) -> dict[Offset, list[TokenFunc]]: 75 | initial_state = State( 76 | settings=settings, 77 | from_imports=collections.defaultdict(set), 78 | ) 79 | 80 | nodes: list[tuple[State, ast.AST, ast.AST]] = [(initial_state, tree, tree)] 81 | 82 | ret = collections.defaultdict(list) 83 | while nodes: 84 | state, node, parent = nodes.pop() 85 | 86 | tp = type(node) 87 | for ast_func in funcs[tp]: 88 | for offset, token_func in ast_func(state, node, parent): 89 | ret[offset].append(token_func) 90 | 91 | if ( 92 | isinstance(node, ast.ImportFrom) and 93 | not node.level and 94 | node.module in RECORD_FROM_IMPORTS 95 | ): 96 | state.from_imports[node.module].update( 97 | name.name for name in node.names if not name.asname 98 | ) 99 | 100 | for name in reversed(node._fields): 101 | value = getattr(node, name) 102 | if name in {'annotation', 'returns'}: 103 | next_state = state._replace(in_annotation=True) 104 | else: 105 | next_state = state 106 | 107 | if isinstance(value, ast.AST): 108 | nodes.append((next_state, value, node)) 109 | elif isinstance(value, list): 110 | for value in reversed(value): 111 | if isinstance(value, ast.AST): 112 | nodes.append((next_state, value, node)) 113 | return ret 114 | 115 | 116 | def _import_plugins() -> None: 117 | plugins_path = _plugins.__path__ 118 | mod_infos = pkgutil.walk_packages(plugins_path, f'{_plugins.__name__}.') 119 | for _, name, _ in mod_infos: 120 | __import__(name, fromlist=['_trash']) 121 | 122 | 123 | _import_plugins() 124 | -------------------------------------------------------------------------------- /pyupgrade/_plugins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asottile/pyupgrade/1a0b8a1996416e51e374431887b12aae3f8d3e80/pyupgrade/_plugins/__init__.py -------------------------------------------------------------------------------- /pyupgrade/_plugins/collections_abc.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import ast 4 | import functools 5 | from collections.abc import Iterable 6 | 7 | from tokenize_rt import Offset 8 | 9 | from pyupgrade._ast_helpers import ast_to_offset 10 | from pyupgrade._data import register 11 | from pyupgrade._data import State 12 | from pyupgrade._data import TokenFunc 13 | from pyupgrade._plugins.imports import REPLACE_EXACT 14 | from pyupgrade._token_helpers import replace_name 15 | 16 | COLLECTIONS_ABC_ATTRS = frozenset( 17 | attr for mod, attr in REPLACE_EXACT[(3,)] if mod == 'collections' 18 | ) 19 | 20 | 21 | @register(ast.Attribute) 22 | def visit_Attribute( 23 | state: State, 24 | node: ast.Attribute, 25 | parent: ast.AST, 26 | ) -> Iterable[tuple[Offset, TokenFunc]]: 27 | if ( 28 | isinstance(node.value, ast.Name) and 29 | node.value.id == 'collections' and 30 | node.attr in COLLECTIONS_ABC_ATTRS 31 | ): 32 | new_attr = f'collections.abc.{node.attr}' 33 | func = functools.partial(replace_name, name=node.attr, new=new_attr) 34 | yield ast_to_offset(node), func 35 | -------------------------------------------------------------------------------- /pyupgrade/_plugins/constant_fold.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import ast 4 | from collections.abc import Iterable 5 | 6 | from tokenize_rt import Offset 7 | 8 | from pyupgrade._ast_helpers import ast_to_offset 9 | from pyupgrade._ast_helpers import is_type_check 10 | from pyupgrade._data import register 11 | from pyupgrade._data import State 12 | from pyupgrade._data import TokenFunc 13 | from pyupgrade._token_helpers import constant_fold_tuple 14 | 15 | 16 | def _to_name(node: ast.AST) -> str | None: 17 | if isinstance(node, ast.Name): 18 | return node.id 19 | elif isinstance(node, ast.Attribute): 20 | base = _to_name(node.value) 21 | if base is None: 22 | return None 23 | else: 24 | return f'{base}.{node.attr}' 25 | else: 26 | return None 27 | 28 | 29 | def _can_constant_fold(node: ast.Tuple) -> bool: 30 | seen = set() 31 | for el in node.elts: 32 | name = _to_name(el) 33 | if name is not None: 34 | if name in seen: 35 | return True 36 | else: 37 | seen.add(name) 38 | else: 39 | return False 40 | 41 | 42 | def _cbs(node: ast.AST | None) -> Iterable[tuple[Offset, TokenFunc]]: 43 | if isinstance(node, ast.Tuple) and _can_constant_fold(node): 44 | yield ast_to_offset(node), constant_fold_tuple 45 | 46 | 47 | @register(ast.Call) 48 | def visit_Call( 49 | state: State, 50 | node: ast.Call, 51 | parent: ast.AST, 52 | ) -> Iterable[tuple[Offset, TokenFunc]]: 53 | if is_type_check(node): 54 | yield from _cbs(node.args[1]) 55 | 56 | 57 | @register(ast.Try) 58 | def visit_Try( 59 | state: State, 60 | node: ast.Try, 61 | parent: ast.AST, 62 | ) -> Iterable[tuple[Offset, TokenFunc]]: 63 | for handler in node.handlers: 64 | yield from _cbs(handler.type) 65 | -------------------------------------------------------------------------------- /pyupgrade/_plugins/datetime_utc_alias.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import ast 4 | import functools 5 | from collections.abc import Iterable 6 | 7 | from tokenize_rt import Offset 8 | 9 | from pyupgrade._ast_helpers import ast_to_offset 10 | from pyupgrade._data import register 11 | from pyupgrade._data import State 12 | from pyupgrade._data import TokenFunc 13 | from pyupgrade._token_helpers import replace_name 14 | 15 | 16 | @register(ast.Attribute) 17 | def visit_Attribute( 18 | state: State, 19 | node: ast.Attribute, 20 | parent: ast.AST, 21 | ) -> Iterable[tuple[Offset, TokenFunc]]: 22 | if ( 23 | state.settings.min_version >= (3, 11) and 24 | node.attr == 'utc' and 25 | isinstance(node.value, ast.Attribute) and 26 | node.value.attr == 'timezone' and 27 | isinstance(node.value.value, ast.Name) and 28 | node.value.value.id == 'datetime' 29 | ): 30 | func = functools.partial( 31 | replace_name, 32 | name='utc', 33 | new='datetime.UTC', 34 | ) 35 | yield ast_to_offset(node), func 36 | -------------------------------------------------------------------------------- /pyupgrade/_plugins/default_encoding.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import ast 4 | from collections.abc import Iterable 5 | 6 | from tokenize_rt import Offset 7 | from tokenize_rt import Token 8 | 9 | from pyupgrade._ast_helpers import ast_to_offset 10 | from pyupgrade._ast_helpers import has_starargs 11 | from pyupgrade._data import register 12 | from pyupgrade._data import State 13 | from pyupgrade._data import TokenFunc 14 | from pyupgrade._string_helpers import is_codec 15 | from pyupgrade._token_helpers import find_call 16 | from pyupgrade._token_helpers import find_closing_bracket 17 | 18 | 19 | def _fix_default_encoding(i: int, tokens: list[Token]) -> None: 20 | i = find_call(tokens, i + 1) 21 | j = find_closing_bracket(tokens, i) 22 | del tokens[i + 1:j] 23 | 24 | 25 | @register(ast.Call) 26 | def visit_Call( 27 | state: State, 28 | node: ast.Call, 29 | parent: ast.AST, 30 | ) -> Iterable[tuple[Offset, TokenFunc]]: 31 | if ( 32 | isinstance(node.func, ast.Attribute) and ( 33 | ( 34 | isinstance(node.func.value, ast.Constant) and 35 | isinstance(node.func.value.value, str) 36 | ) or 37 | isinstance(node.func.value, ast.JoinedStr) 38 | ) and 39 | node.func.attr == 'encode' and 40 | not has_starargs(node) and 41 | len(node.args) == 1 and 42 | isinstance(node.args[0], ast.Constant) and 43 | isinstance(node.args[0].value, str) and 44 | is_codec(node.args[0].value, 'utf-8') 45 | ): 46 | yield ast_to_offset(node), _fix_default_encoding 47 | -------------------------------------------------------------------------------- /pyupgrade/_plugins/defaultdict_lambda.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import ast 4 | import functools 5 | from collections.abc import Iterable 6 | 7 | from tokenize_rt import Offset 8 | from tokenize_rt import Token 9 | 10 | from pyupgrade._ast_helpers import ast_to_offset 11 | from pyupgrade._ast_helpers import is_name_attr 12 | from pyupgrade._data import register 13 | from pyupgrade._data import State 14 | from pyupgrade._data import TokenFunc 15 | from pyupgrade._token_helpers import find_op 16 | from pyupgrade._token_helpers import parse_call_args 17 | 18 | 19 | def _eligible_lambda_replacement(lambda_expr: ast.Lambda) -> str | None: 20 | if isinstance(lambda_expr.body, ast.Constant): 21 | if lambda_expr.body.value == 0: 22 | return type(lambda_expr.body.value).__name__ 23 | elif lambda_expr.body.value == '': 24 | return 'str' 25 | else: 26 | return None 27 | elif isinstance(lambda_expr.body, ast.List) and not lambda_expr.body.elts: 28 | return 'list' 29 | elif isinstance(lambda_expr.body, ast.Tuple) and not lambda_expr.body.elts: 30 | return 'tuple' 31 | elif isinstance(lambda_expr.body, ast.Dict) and not lambda_expr.body.keys: 32 | return 'dict' 33 | elif ( 34 | isinstance(lambda_expr.body, ast.Call) and 35 | isinstance(lambda_expr.body.func, ast.Name) and 36 | not lambda_expr.body.args and 37 | not lambda_expr.body.keywords and 38 | lambda_expr.body.func.id in {'dict', 'list', 'set', 'tuple'} 39 | ): 40 | return lambda_expr.body.func.id 41 | else: 42 | return None 43 | 44 | 45 | def _fix_defaultdict_first_arg( 46 | i: int, 47 | tokens: list[Token], 48 | *, 49 | replacement: str, 50 | ) -> None: 51 | start = find_op(tokens, i, '(') 52 | func_args, end = parse_call_args(tokens, start) 53 | 54 | tokens[slice(*func_args[0])] = [Token('CODE', replacement)] 55 | 56 | 57 | @register(ast.Call) 58 | def visit_Call( 59 | state: State, 60 | node: ast.Call, 61 | parent: ast.AST, 62 | ) -> Iterable[tuple[Offset, TokenFunc]]: 63 | if ( 64 | is_name_attr( 65 | node.func, 66 | state.from_imports, 67 | ('collections',), 68 | ('defaultdict',), 69 | ) and 70 | node.args and 71 | isinstance(node.args[0], ast.Lambda) 72 | ): 73 | replacement = _eligible_lambda_replacement(node.args[0]) 74 | if replacement is None: 75 | return 76 | 77 | func = functools.partial( 78 | _fix_defaultdict_first_arg, 79 | replacement=replacement, 80 | ) 81 | yield ast_to_offset(node), func 82 | -------------------------------------------------------------------------------- /pyupgrade/_plugins/dict_literals.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import ast 4 | import functools 5 | from collections.abc import Iterable 6 | 7 | from tokenize_rt import Offset 8 | from tokenize_rt import Token 9 | from tokenize_rt import UNIMPORTANT_WS 10 | 11 | from pyupgrade._ast_helpers import ast_to_offset 12 | from pyupgrade._data import register 13 | from pyupgrade._data import State 14 | from pyupgrade._data import TokenFunc 15 | from pyupgrade._token_helpers import immediately_paren 16 | from pyupgrade._token_helpers import remove_brace 17 | from pyupgrade._token_helpers import victims 18 | 19 | 20 | def _fix_dict_comp( 21 | i: int, 22 | tokens: list[Token], 23 | arg: ast.ListComp | ast.GeneratorExp, 24 | ) -> None: 25 | if not immediately_paren('dict', tokens, i): 26 | return 27 | 28 | dict_victims = victims(tokens, i + 1, arg, gen=True) 29 | elt_victims = victims(tokens, dict_victims.arg_index, arg.elt, gen=True) 30 | 31 | del dict_victims.starts[0] 32 | end_index = dict_victims.ends.pop() 33 | 34 | tokens[end_index] = Token('OP', '}') 35 | for index in reversed(dict_victims.ends): 36 | remove_brace(tokens, index) 37 | # See #6, Fix SyntaxError from rewriting dict((a, b)for a, b in y) 38 | if tokens[elt_victims.ends[-1] + 1].src == 'for': 39 | tokens.insert(elt_victims.ends[-1] + 1, Token(UNIMPORTANT_WS, ' ')) 40 | for index in reversed(elt_victims.ends): 41 | remove_brace(tokens, index) 42 | assert elt_victims.first_comma_index is not None 43 | tokens[elt_victims.first_comma_index] = Token('OP', ':') 44 | for index in reversed(dict_victims.starts + elt_victims.starts): 45 | remove_brace(tokens, index) 46 | tokens[i:i + 2] = [Token('OP', '{')] 47 | 48 | 49 | @register(ast.Call) 50 | def visit_Call( 51 | state: State, 52 | node: ast.Call, 53 | parent: ast.AST, 54 | ) -> Iterable[tuple[Offset, TokenFunc]]: 55 | if ( 56 | not isinstance(parent, ast.FormattedValue) and 57 | isinstance(node.func, ast.Name) and 58 | node.func.id == 'dict' and 59 | len(node.args) == 1 and 60 | not node.keywords and 61 | isinstance(node.args[0], (ast.ListComp, ast.GeneratorExp)) and 62 | isinstance(node.args[0].elt, (ast.Tuple, ast.List)) and 63 | len(node.args[0].elt.elts) == 2 64 | ): 65 | func = functools.partial(_fix_dict_comp, arg=node.args[0]) 66 | yield ast_to_offset(node.func), func 67 | -------------------------------------------------------------------------------- /pyupgrade/_plugins/exceptions.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import ast 4 | import functools 5 | from collections.abc import Iterable 6 | from typing import NamedTuple 7 | 8 | from tokenize_rt import Offset 9 | from tokenize_rt import Token 10 | 11 | from pyupgrade._ast_helpers import ast_to_offset 12 | from pyupgrade._data import register 13 | from pyupgrade._data import State 14 | from pyupgrade._data import TokenFunc 15 | from pyupgrade._data import Version 16 | from pyupgrade._token_helpers import constant_fold_tuple 17 | from pyupgrade._token_helpers import find_op 18 | from pyupgrade._token_helpers import parse_call_args 19 | from pyupgrade._token_helpers import replace_name 20 | 21 | 22 | class _Target(NamedTuple): 23 | target: str 24 | module: str | None 25 | name: str 26 | min_version: Version 27 | 28 | 29 | _TARGETS = ( 30 | _Target('OSError', 'mmap', 'error', (3,)), 31 | _Target('OSError', 'os', 'error', (3,)), 32 | _Target('OSError', 'select', 'error', (3,)), 33 | _Target('OSError', 'socket', 'error', (3,)), 34 | _Target('OSError', None, 'IOError', (3,)), 35 | _Target('OSError', None, 'EnvironmentError', (3,)), 36 | _Target('OSError', None, 'WindowsError', (3,)), 37 | _Target('TimeoutError', 'socket', 'timeout', (3, 10)), 38 | _Target('TimeoutError', 'asyncio', 'TimeoutError', (3, 11)), 39 | ) 40 | 41 | 42 | def _fix_except( 43 | i: int, 44 | tokens: list[Token], 45 | *, 46 | at_idx: dict[int, _Target], 47 | ) -> None: 48 | start = find_op(tokens, i, '(') 49 | func_args, end = parse_call_args(tokens, start) 50 | 51 | for i, target in reversed(at_idx.items()): 52 | tokens[slice(*func_args[i])] = [Token('NAME', target.target)] 53 | 54 | constant_fold_tuple(start, tokens) 55 | 56 | 57 | def _get_rewrite( 58 | node: ast.AST, 59 | state: State, 60 | targets: list[_Target], 61 | ) -> _Target | None: 62 | for target in targets: 63 | if ( 64 | target.module is None and 65 | isinstance(node, ast.Name) and 66 | node.id == target.name 67 | ): 68 | return target 69 | elif ( 70 | target.module is not None and 71 | isinstance(node, ast.Name) and 72 | node.id == target.name and 73 | node.id in state.from_imports[target.module] 74 | ): 75 | return target 76 | elif ( 77 | target.module is not None and 78 | isinstance(node, ast.Attribute) and 79 | isinstance(node.value, ast.Name) and 80 | node.attr == target.name and 81 | node.value.id == target.module 82 | ): 83 | return target 84 | else: 85 | return None 86 | 87 | 88 | def _alias_cbs( 89 | node: ast.expr, 90 | state: State, 91 | targets: list[_Target], 92 | ) -> Iterable[tuple[Offset, TokenFunc]]: 93 | target = _get_rewrite(node, state, targets) 94 | if target is not None: 95 | func = functools.partial( 96 | replace_name, 97 | name=target.name, 98 | new=target.target, 99 | ) 100 | yield ast_to_offset(node), func 101 | 102 | 103 | @register(ast.Raise) 104 | def visit_Raise( 105 | state: State, 106 | node: ast.Raise, 107 | parent: ast.AST, 108 | ) -> Iterable[tuple[Offset, TokenFunc]]: 109 | targets = [ 110 | target for target in _TARGETS 111 | if state.settings.min_version >= target.min_version 112 | ] 113 | if node.exc is not None: 114 | yield from _alias_cbs(node.exc, state, targets) 115 | if isinstance(node.exc, ast.Call): 116 | yield from _alias_cbs(node.exc.func, state, targets) 117 | 118 | 119 | @register(ast.Try) 120 | def visit_Try( 121 | state: State, 122 | node: ast.Try, 123 | parent: ast.AST, 124 | ) -> Iterable[tuple[Offset, TokenFunc]]: 125 | targets = [ 126 | target for target in _TARGETS 127 | if state.settings.min_version >= target.min_version 128 | ] 129 | for handler in node.handlers: 130 | if isinstance(handler.type, ast.Tuple): 131 | at_idx = {} 132 | for i, elt in enumerate(handler.type.elts): 133 | target = _get_rewrite(elt, state, targets) 134 | if target is not None: 135 | at_idx[i] = target 136 | 137 | if at_idx: 138 | func = functools.partial(_fix_except, at_idx=at_idx) 139 | yield ast_to_offset(handler.type), func 140 | elif handler.type is not None: 141 | yield from _alias_cbs(handler.type, state, targets) 142 | -------------------------------------------------------------------------------- /pyupgrade/_plugins/format_locals.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import ast 4 | from collections.abc import Iterable 5 | 6 | from tokenize_rt import Offset 7 | from tokenize_rt import rfind_string_parts 8 | from tokenize_rt import Token 9 | 10 | from pyupgrade._ast_helpers import ast_to_offset 11 | from pyupgrade._data import register 12 | from pyupgrade._data import State 13 | from pyupgrade._data import TokenFunc 14 | from pyupgrade._token_helpers import find_closing_bracket 15 | from pyupgrade._token_helpers import find_op 16 | 17 | 18 | def _fix(i: int, tokens: list[Token]) -> None: 19 | dot_pos = find_op(tokens, i, '.') 20 | open_pos = find_op(tokens, dot_pos, '(') 21 | close_pos = find_closing_bracket(tokens, open_pos) 22 | for string_idx in rfind_string_parts(tokens, dot_pos - 1): 23 | tok = tokens[string_idx] 24 | tokens[string_idx] = tok._replace(src=f'f{tok.src}') 25 | del tokens[dot_pos:close_pos + 1] 26 | 27 | 28 | @register(ast.Call) 29 | def visit_Call( 30 | state: State, 31 | node: ast.Call, 32 | parent: ast.AST, 33 | ) -> Iterable[tuple[Offset, TokenFunc]]: 34 | if ( 35 | state.settings.min_version >= (3, 6) and 36 | isinstance(node.func, ast.Attribute) and 37 | isinstance(node.func.value, ast.Constant) and 38 | isinstance(node.func.value.value, str) and 39 | node.func.attr == 'format' and 40 | len(node.args) == 0 and 41 | len(node.keywords) == 1 and 42 | node.keywords[0].arg is None and 43 | isinstance(node.keywords[0].value, ast.Call) and 44 | isinstance(node.keywords[0].value.func, ast.Name) and 45 | node.keywords[0].value.func.id == 'locals' and 46 | len(node.keywords[0].value.args) == 0 and 47 | len(node.keywords[0].value.keywords) == 0 48 | ): 49 | yield ast_to_offset(node), _fix 50 | -------------------------------------------------------------------------------- /pyupgrade/_plugins/fstrings.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import ast 4 | from collections.abc import Iterable 5 | 6 | from tokenize_rt import Offset 7 | from tokenize_rt import parse_string_literal 8 | from tokenize_rt import Token 9 | from tokenize_rt import tokens_to_src 10 | 11 | from pyupgrade._ast_helpers import ast_to_offset 12 | from pyupgrade._ast_helpers import contains_await 13 | from pyupgrade._ast_helpers import has_starargs 14 | from pyupgrade._data import register 15 | from pyupgrade._data import State 16 | from pyupgrade._data import TokenFunc 17 | from pyupgrade._string_helpers import parse_format 18 | from pyupgrade._string_helpers import unparse_parsed_string 19 | from pyupgrade._token_helpers import parse_call_args 20 | 21 | 22 | def _skip_unimportant_ws(tokens: list[Token], i: int) -> int: 23 | while tokens[i].name == 'UNIMPORTANT_WS': 24 | i += 1 25 | return i 26 | 27 | 28 | def _to_fstring( 29 | src: str, tokens: list[Token], args: list[tuple[int, int]], 30 | ) -> str: 31 | params = {} 32 | i = 0 33 | for start, end in args: 34 | start = _skip_unimportant_ws(tokens, start) 35 | if tokens[start].name == 'NAME': 36 | after = _skip_unimportant_ws(tokens, start + 1) 37 | if tokens[after].src == '=': # keyword argument 38 | params[tokens[start].src] = tokens_to_src( 39 | tokens[after + 1:end], 40 | ).strip() 41 | continue 42 | params[str(i)] = tokens_to_src(tokens[start:end]).strip() 43 | i += 1 44 | 45 | parts = [] 46 | i = 0 47 | 48 | # need to remove `u` prefix so it isn't invalid syntax 49 | prefix, rest = parse_string_literal(src) 50 | new_src = 'f' + prefix.translate({ord('u'): None, ord('U'): None}) + rest 51 | 52 | for s, name, spec, conv in parse_format(new_src): 53 | if name is not None: 54 | k, dot, rest = name.partition('.') 55 | name = ''.join((params[k or str(i)], dot, rest)) 56 | if not k: # named and auto params can be in different orders 57 | i += 1 58 | parts.append((s, name, spec, conv)) 59 | return unparse_parsed_string(parts) 60 | 61 | 62 | def _fix_fstring(i: int, tokens: list[Token]) -> None: 63 | token = tokens[i] 64 | 65 | paren = i + 3 66 | if tokens_to_src(tokens[i + 1:paren + 1]) != '.format(': 67 | return 68 | 69 | args, end = parse_call_args(tokens, paren) 70 | # if it spans more than one line, bail 71 | if tokens[end - 1].line != token.line: 72 | return 73 | 74 | args_src = tokens_to_src(tokens[paren:end]) 75 | if '\\' in args_src or '"' in args_src or "'" in args_src: 76 | return 77 | 78 | tokens[i] = token._replace(src=_to_fstring(token.src, tokens, args)) 79 | del tokens[i + 1:end] 80 | 81 | 82 | def _format_params(call: ast.Call) -> set[str]: 83 | params = {str(i) for i, arg in enumerate(call.args)} 84 | for kwd in call.keywords: 85 | # kwd.arg can't be None here because we exclude starargs 86 | assert kwd.arg is not None 87 | params.add(kwd.arg) 88 | return params 89 | 90 | 91 | @register(ast.Call) 92 | def visit_Call( 93 | state: State, 94 | node: ast.Call, 95 | parent: ast.AST, 96 | ) -> Iterable[tuple[Offset, TokenFunc]]: 97 | if state.settings.min_version < (3, 6): 98 | return 99 | 100 | if ( 101 | isinstance(node.func, ast.Attribute) and 102 | isinstance(node.func.value, ast.Constant) and 103 | isinstance(node.func.value.value, str) and 104 | node.func.attr == 'format' and 105 | not has_starargs(node) 106 | ): 107 | try: 108 | parsed = parse_format(node.func.value.value) 109 | except ValueError: 110 | return 111 | 112 | params = _format_params(node) 113 | seen = set() 114 | i = 0 115 | for _, name, spec, _ in parsed: 116 | # timid: difficult to rewrite correctly 117 | if spec is not None and '{' in spec: 118 | break 119 | if name is not None: 120 | candidate, _, _ = name.partition('.') 121 | # timid: could make the f-string longer 122 | if candidate and candidate in seen: 123 | break 124 | # timid: bracketed 125 | elif '[' in name: 126 | break 127 | seen.add(candidate) 128 | 129 | key = candidate or str(i) 130 | # their .format() call is broken currently 131 | if key not in params: 132 | break 133 | if not candidate: 134 | i += 1 135 | else: 136 | if ( 137 | state.settings.min_version >= (3, 7) or 138 | not contains_await(node) 139 | ): 140 | yield ast_to_offset(node), _fix_fstring 141 | -------------------------------------------------------------------------------- /pyupgrade/_plugins/identity_equality.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import ast 4 | import functools 5 | from collections.abc import Iterable 6 | 7 | from tokenize_rt import Offset 8 | from tokenize_rt import Token 9 | 10 | from pyupgrade._ast_helpers import ast_to_offset 11 | from pyupgrade._data import register 12 | from pyupgrade._data import State 13 | from pyupgrade._data import TokenFunc 14 | 15 | 16 | def _fix_is_literal( 17 | i: int, 18 | tokens: list[Token], 19 | *, 20 | op: ast.Is | ast.IsNot, 21 | ) -> None: 22 | while tokens[i].src != 'is': 23 | i -= 1 24 | if isinstance(op, ast.Is): 25 | tokens[i] = tokens[i]._replace(src='==') 26 | else: 27 | tokens[i] = tokens[i]._replace(src='!=') 28 | # since we iterate backward, the empty tokens keep the same length 29 | i += 1 30 | while tokens[i].src != 'not': 31 | tokens[i] = Token('EMPTY', '') 32 | i += 1 33 | tokens[i] = Token('EMPTY', '') 34 | 35 | 36 | def _is_literal(n: ast.AST) -> bool: 37 | return ( 38 | isinstance(n, ast.Constant) and 39 | n.value not in {True, False} and 40 | isinstance(n.value, (str, bytes, int, float)) 41 | ) 42 | 43 | 44 | @register(ast.Compare) 45 | def visit_Compare( 46 | state: State, 47 | node: ast.Compare, 48 | parent: ast.AST, 49 | ) -> Iterable[tuple[Offset, TokenFunc]]: 50 | left = node.left 51 | for op, right in zip(node.ops, node.comparators): 52 | if ( 53 | isinstance(op, (ast.Is, ast.IsNot)) and 54 | (_is_literal(left) or _is_literal(right)) 55 | ): 56 | func = functools.partial(_fix_is_literal, op=op) 57 | yield ast_to_offset(right), func 58 | left = right 59 | -------------------------------------------------------------------------------- /pyupgrade/_plugins/io_open.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import ast 4 | from collections.abc import Iterable 5 | 6 | from tokenize_rt import Offset 7 | from tokenize_rt import Token 8 | 9 | from pyupgrade._ast_helpers import ast_to_offset 10 | from pyupgrade._data import register 11 | from pyupgrade._data import State 12 | from pyupgrade._data import TokenFunc 13 | from pyupgrade._token_helpers import find_op 14 | 15 | 16 | def _replace_io_open(i: int, tokens: list[Token]) -> None: 17 | j = find_op(tokens, i, '(') 18 | tokens[i:j] = [tokens[i]._replace(name='NAME', src='open')] 19 | 20 | 21 | @register(ast.Call) 22 | def visit_Call( 23 | state: State, 24 | node: ast.Call, 25 | parent: ast.AST, 26 | ) -> Iterable[tuple[Offset, TokenFunc]]: 27 | if ( 28 | isinstance(node.func, ast.Attribute) and 29 | isinstance(node.func.value, ast.Name) and 30 | node.func.value.id == 'io' and 31 | node.func.attr == 'open' 32 | ): 33 | yield ast_to_offset(node.func), _replace_io_open 34 | -------------------------------------------------------------------------------- /pyupgrade/_plugins/lru_cache.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import ast 4 | import functools 5 | from collections.abc import Iterable 6 | 7 | from tokenize_rt import Offset 8 | from tokenize_rt import Token 9 | 10 | from pyupgrade._ast_helpers import ast_to_offset 11 | from pyupgrade._ast_helpers import is_name_attr 12 | from pyupgrade._data import register 13 | from pyupgrade._data import State 14 | from pyupgrade._data import TokenFunc 15 | from pyupgrade._token_helpers import find_and_replace_call 16 | from pyupgrade._token_helpers import find_op 17 | 18 | 19 | def _remove_call(i: int, tokens: list[Token]) -> None: 20 | i = find_op(tokens, i, '(') 21 | j = find_op(tokens, i, ')') 22 | del tokens[i:j + 1] 23 | 24 | 25 | def _is_literal_kwarg( 26 | keyword: ast.keyword, name: str, value: bool | None, 27 | ) -> bool: 28 | return ( 29 | keyword.arg == name and 30 | isinstance(keyword.value, ast.Constant) and 31 | keyword.value.value is value 32 | ) 33 | 34 | 35 | def _eligible(keywords: list[ast.keyword]) -> bool: 36 | if len(keywords) == 1: 37 | return _is_literal_kwarg(keywords[0], 'maxsize', None) 38 | elif len(keywords) == 2: 39 | return ( 40 | ( 41 | _is_literal_kwarg(keywords[0], 'maxsize', None) and 42 | _is_literal_kwarg(keywords[1], 'typed', False) 43 | ) or 44 | ( 45 | _is_literal_kwarg(keywords[1], 'maxsize', None) and 46 | _is_literal_kwarg(keywords[0], 'typed', False) 47 | ) 48 | ) 49 | else: 50 | return False 51 | 52 | 53 | @register(ast.Call) 54 | def visit_Call( 55 | state: State, 56 | node: ast.Call, 57 | parent: ast.AST, 58 | ) -> Iterable[tuple[Offset, TokenFunc]]: 59 | if ( 60 | state.settings.min_version >= (3, 8) and 61 | not node.args and 62 | not node.keywords and 63 | is_name_attr( 64 | node.func, 65 | state.from_imports, 66 | ('functools',), 67 | ('lru_cache',), 68 | ) 69 | ): 70 | yield ast_to_offset(node), _remove_call 71 | elif ( 72 | state.settings.min_version >= (3, 9) and 73 | isinstance(node.func, ast.Attribute) and 74 | node.func.attr == 'lru_cache' and 75 | isinstance(node.func.value, ast.Name) and 76 | node.func.value.id == 'functools' and 77 | not node.args and 78 | _eligible(node.keywords) 79 | ): 80 | func = functools.partial( 81 | find_and_replace_call, template='functools.cache', 82 | ) 83 | yield ast_to_offset(node), func 84 | -------------------------------------------------------------------------------- /pyupgrade/_plugins/metaclass_type.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import ast 4 | from collections.abc import Iterable 5 | 6 | from tokenize_rt import Offset 7 | from tokenize_rt import Token 8 | 9 | from pyupgrade._ast_helpers import ast_to_offset 10 | from pyupgrade._data import register 11 | from pyupgrade._data import State 12 | from pyupgrade._data import TokenFunc 13 | from pyupgrade._token_helpers import find_end 14 | 15 | 16 | def _remove_metaclass_type(i: int, tokens: list[Token]) -> None: 17 | j = find_end(tokens, i) 18 | del tokens[i:j] 19 | 20 | 21 | @register(ast.Assign) 22 | def visit_Assign( 23 | state: State, 24 | node: ast.Assign, 25 | parent: ast.AST, 26 | ) -> Iterable[tuple[Offset, TokenFunc]]: 27 | if ( 28 | len(node.targets) == 1 and 29 | isinstance(node.targets[0], ast.Name) and 30 | node.targets[0].col_offset == 0 and 31 | node.targets[0].id == '__metaclass__' and 32 | isinstance(node.value, ast.Name) and 33 | node.value.id == 'type' 34 | ): 35 | yield ast_to_offset(node), _remove_metaclass_type 36 | -------------------------------------------------------------------------------- /pyupgrade/_plugins/mock.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import ast 4 | from collections.abc import Iterable 5 | 6 | from tokenize_rt import Offset 7 | from tokenize_rt import Token 8 | 9 | from pyupgrade._ast_helpers import ast_to_offset 10 | from pyupgrade._data import register 11 | from pyupgrade._data import State 12 | from pyupgrade._data import TokenFunc 13 | from pyupgrade._token_helpers import find_name 14 | 15 | 16 | def _fix_mock_mock(i: int, tokens: list[Token]) -> None: 17 | j = find_name(tokens, i + 1, 'mock') 18 | del tokens[i + 1:j + 1] 19 | 20 | 21 | @register(ast.Attribute) 22 | def visit_Attribute( 23 | state: State, 24 | node: ast.Attribute, 25 | parent: ast.AST, 26 | ) -> Iterable[tuple[Offset, TokenFunc]]: 27 | if ( 28 | not state.settings.keep_mock and 29 | isinstance(node.value, ast.Name) and 30 | node.value.id == 'mock' and 31 | node.attr == 'mock' 32 | ): 33 | yield ast_to_offset(node), _fix_mock_mock 34 | -------------------------------------------------------------------------------- /pyupgrade/_plugins/native_literals.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import ast 4 | import functools 5 | from collections.abc import Iterable 6 | 7 | from tokenize_rt import Offset 8 | from tokenize_rt import Token 9 | 10 | from pyupgrade._ast_helpers import ast_to_offset 11 | from pyupgrade._ast_helpers import has_starargs 12 | from pyupgrade._ast_helpers import is_name_attr 13 | from pyupgrade._data import register 14 | from pyupgrade._data import State 15 | from pyupgrade._data import TokenFunc 16 | from pyupgrade._token_helpers import find_op 17 | from pyupgrade._token_helpers import parse_call_args 18 | from pyupgrade._token_helpers import replace_call 19 | 20 | SIX_NATIVE_STR = frozenset(('ensure_str', 'ensure_text', 'text_type')) 21 | 22 | 23 | def _fix_literal(i: int, tokens: list[Token], *, empty: str) -> None: 24 | j = find_op(tokens, i, '(') 25 | func_args, end = parse_call_args(tokens, j) 26 | if any(tok.name == 'NL' for tok in tokens[i:end]): 27 | return 28 | if func_args: 29 | replace_call(tokens, i, end, func_args, '{args[0]}') 30 | else: 31 | tokens[i:end] = [tokens[i]._replace(name='STRING', src=empty)] 32 | 33 | 34 | def is_a_native_literal_call( 35 | node: ast.Call, 36 | from_imports: dict[str, set[str]], 37 | ) -> bool: 38 | return ( 39 | ( 40 | is_name_attr(node.func, from_imports, ('six',), SIX_NATIVE_STR) or 41 | isinstance(node.func, ast.Name) and node.func.id == 'str' 42 | ) and 43 | not node.keywords and 44 | not has_starargs(node) and 45 | ( 46 | len(node.args) == 0 or 47 | ( 48 | len(node.args) == 1 and 49 | isinstance(node.args[0], ast.Constant) and 50 | isinstance(node.args[0].value, str) 51 | ) 52 | ) 53 | ) 54 | 55 | 56 | @register(ast.Call) 57 | def visit_Call( 58 | state: State, 59 | node: ast.Call, 60 | parent: ast.AST, 61 | ) -> Iterable[tuple[Offset, TokenFunc]]: 62 | if is_a_native_literal_call(node, state.from_imports): 63 | func = functools.partial(_fix_literal, empty="''") 64 | yield ast_to_offset(node), func 65 | elif ( 66 | isinstance(node.func, ast.Name) and node.func.id == 'bytes' and 67 | not node.keywords and not has_starargs(node) and 68 | ( 69 | len(node.args) == 0 or ( 70 | len(node.args) == 1 and 71 | isinstance(node.args[0], ast.Constant) and 72 | isinstance(node.args[0].value, bytes) 73 | ) 74 | ) 75 | ): 76 | func = functools.partial(_fix_literal, empty="b''") 77 | yield ast_to_offset(node), func 78 | -------------------------------------------------------------------------------- /pyupgrade/_plugins/new_style_classes.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import ast 4 | from collections.abc import Iterable 5 | 6 | from tokenize_rt import Offset 7 | 8 | from pyupgrade._ast_helpers import ast_to_offset 9 | from pyupgrade._data import register 10 | from pyupgrade._data import State 11 | from pyupgrade._data import TokenFunc 12 | from pyupgrade._token_helpers import remove_base_class 13 | 14 | 15 | @register(ast.ClassDef) 16 | def visit_ClassDef( 17 | state: State, 18 | node: ast.ClassDef, 19 | parent: ast.AST, 20 | ) -> Iterable[tuple[Offset, TokenFunc]]: 21 | for base in node.bases: 22 | if isinstance(base, ast.Name) and base.id == 'object': 23 | yield ast_to_offset(base), remove_base_class 24 | -------------------------------------------------------------------------------- /pyupgrade/_plugins/open_mode.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import ast 4 | import functools 5 | import itertools 6 | from collections.abc import Iterable 7 | from typing import NamedTuple 8 | 9 | from tokenize_rt import Offset 10 | from tokenize_rt import Token 11 | from tokenize_rt import tokens_to_src 12 | 13 | from pyupgrade._ast_helpers import ast_to_offset 14 | from pyupgrade._ast_helpers import has_starargs 15 | from pyupgrade._data import register 16 | from pyupgrade._data import State 17 | from pyupgrade._data import TokenFunc 18 | from pyupgrade._token_helpers import delete_argument 19 | from pyupgrade._token_helpers import find_op 20 | from pyupgrade._token_helpers import parse_call_args 21 | 22 | 23 | def _plus(args: tuple[str, ...]) -> tuple[str, ...]: 24 | return args + tuple(f'{arg}+' for arg in args) 25 | 26 | 27 | def _permute(*args: str) -> tuple[str, ...]: 28 | return tuple(''.join(p) for s in args for p in itertools.permutations(s)) 29 | 30 | 31 | MODE_REMOVE = frozenset(_permute('U', 'r', 'rU', 'rt')) 32 | MODE_REPLACE_R = frozenset(_permute('Ub')) 33 | MODE_REMOVE_T = frozenset(_plus(_permute('at', 'rt', 'wt', 'xt'))) 34 | MODE_REMOVE_U = frozenset(_permute('rUb')) 35 | MODE_REPLACE = MODE_REPLACE_R | MODE_REMOVE_T | MODE_REMOVE_U 36 | 37 | 38 | class FunctionArg(NamedTuple): 39 | arg_idx: int 40 | value: ast.expr 41 | 42 | 43 | def _fix_open_mode(i: int, tokens: list[Token], *, arg_idx: int) -> None: 44 | j = find_op(tokens, i, '(') 45 | func_args, end = parse_call_args(tokens, j) 46 | mode = tokens_to_src(tokens[slice(*func_args[arg_idx])]) 47 | mode_stripped = mode.split('=')[-1] 48 | mode_stripped = ast.literal_eval(mode_stripped.strip()) 49 | if mode_stripped in MODE_REMOVE: 50 | delete_argument(arg_idx, tokens, func_args) 51 | elif mode_stripped in MODE_REPLACE_R: 52 | new_mode = mode.replace('U', 'r') 53 | tokens[slice(*func_args[arg_idx])] = [Token('SRC', new_mode)] 54 | elif mode_stripped in MODE_REMOVE_T: 55 | new_mode = mode.replace('t', '') 56 | tokens[slice(*func_args[arg_idx])] = [Token('SRC', new_mode)] 57 | elif mode_stripped in MODE_REMOVE_U: 58 | new_mode = mode.replace('U', '') 59 | tokens[slice(*func_args[arg_idx])] = [Token('SRC', new_mode)] 60 | else: 61 | raise AssertionError(f'unreachable: {mode!r}') 62 | 63 | 64 | @register(ast.Call) 65 | def visit_Call( 66 | state: State, 67 | node: ast.Call, 68 | parent: ast.AST, 69 | ) -> Iterable[tuple[Offset, TokenFunc]]: 70 | if ( 71 | ( 72 | ( 73 | isinstance(node.func, ast.Name) and 74 | node.func.id == 'open' 75 | ) or ( 76 | isinstance(node.func, ast.Attribute) and 77 | isinstance(node.func.value, ast.Name) and 78 | node.func.value.id == 'io' and 79 | node.func.attr == 'open' 80 | ) 81 | ) and 82 | not has_starargs(node) 83 | ): 84 | if ( 85 | len(node.args) >= 2 and 86 | isinstance(node.args[1], ast.Constant) and 87 | isinstance(node.args[1].value, str) 88 | ): 89 | if ( 90 | node.args[1].value in MODE_REPLACE or 91 | (len(node.args) == 2 and node.args[1].value in MODE_REMOVE) 92 | ): 93 | func = functools.partial(_fix_open_mode, arg_idx=1) 94 | yield ast_to_offset(node), func 95 | elif node.keywords and (len(node.keywords) + len(node.args) > 1): 96 | mode = next( 97 | ( 98 | FunctionArg(n, keyword.value) 99 | for n, keyword in enumerate(node.keywords) 100 | if keyword.arg == 'mode' 101 | ), 102 | None, 103 | ) 104 | if ( 105 | mode is not None and 106 | isinstance(mode.value, ast.Constant) and 107 | isinstance(mode.value.value, str) and 108 | ( 109 | mode.value.value in MODE_REMOVE or 110 | mode.value.value in MODE_REPLACE 111 | ) 112 | ): 113 | func = functools.partial( 114 | _fix_open_mode, 115 | arg_idx=len(node.args) + mode.arg_idx, 116 | ) 117 | yield ast_to_offset(node), func 118 | -------------------------------------------------------------------------------- /pyupgrade/_plugins/set_literals.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import ast 4 | import functools 5 | from collections.abc import Iterable 6 | 7 | from tokenize_rt import Offset 8 | from tokenize_rt import Token 9 | 10 | from pyupgrade._ast_helpers import ast_to_offset 11 | from pyupgrade._data import register 12 | from pyupgrade._data import State 13 | from pyupgrade._data import TokenFunc 14 | from pyupgrade._token_helpers import find_closing_bracket 15 | from pyupgrade._token_helpers import find_op 16 | from pyupgrade._token_helpers import immediately_paren 17 | from pyupgrade._token_helpers import remove_brace 18 | from pyupgrade._token_helpers import victims 19 | 20 | SET_TRANSFORM = (ast.List, ast.ListComp, ast.GeneratorExp, ast.Tuple) 21 | 22 | 23 | def _fix_set_empty_literal(i: int, tokens: list[Token]) -> None: 24 | i = find_op(tokens, i, '(') 25 | j = find_closing_bracket(tokens, i) 26 | del tokens[i + 1:j] 27 | 28 | 29 | def _fix_set_literal(i: int, tokens: list[Token], *, arg: ast.expr) -> None: 30 | # TODO: this could be implemented with a little extra logic 31 | if not immediately_paren('set', tokens, i): 32 | return 33 | 34 | gen = isinstance(arg, ast.GeneratorExp) 35 | set_victims = victims(tokens, i + 1, arg, gen=gen) 36 | 37 | del set_victims.starts[0] 38 | end_index = set_victims.ends.pop() 39 | 40 | tokens[end_index] = Token('OP', '}') 41 | for index in reversed(set_victims.starts + set_victims.ends): 42 | remove_brace(tokens, index) 43 | tokens[i:i + 2] = [Token('OP', '{')] 44 | 45 | 46 | @register(ast.Call) 47 | def visit_Call( 48 | state: State, 49 | node: ast.Call, 50 | parent: ast.AST, 51 | ) -> Iterable[tuple[Offset, TokenFunc]]: 52 | if ( 53 | not isinstance(parent, ast.FormattedValue) and 54 | isinstance(node.func, ast.Name) and 55 | node.func.id == 'set' and 56 | len(node.args) == 1 and 57 | not node.keywords and 58 | isinstance(node.args[0], SET_TRANSFORM) 59 | ): 60 | arg, = node.args 61 | if isinstance(arg, (ast.List, ast.Tuple)) and not arg.elts: 62 | yield ast_to_offset(node.func), _fix_set_empty_literal 63 | else: 64 | func = functools.partial(_fix_set_literal, arg=arg) 65 | yield ast_to_offset(node.func), func 66 | -------------------------------------------------------------------------------- /pyupgrade/_plugins/shlex_join.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import ast 4 | import functools 5 | from collections.abc import Iterable 6 | 7 | from tokenize_rt import NON_CODING_TOKENS 8 | from tokenize_rt import Offset 9 | from tokenize_rt import Token 10 | 11 | from pyupgrade._ast_helpers import ast_to_offset 12 | from pyupgrade._data import register 13 | from pyupgrade._data import State 14 | from pyupgrade._data import TokenFunc 15 | from pyupgrade._token_helpers import find_name 16 | from pyupgrade._token_helpers import find_op 17 | from pyupgrade._token_helpers import victims 18 | 19 | 20 | def _fix_shlex_join(i: int, tokens: list[Token], *, arg: ast.expr) -> None: 21 | j = find_op(tokens, i, '(') 22 | comp_victims = victims(tokens, j, arg, gen=True) 23 | k = find_name(tokens, comp_victims.arg_index, 'in') + 1 24 | while tokens[k].name in NON_CODING_TOKENS: 25 | k += 1 26 | tokens[comp_victims.ends[0]:comp_victims.ends[-1] + 1] = [Token('OP', ')')] 27 | tokens[i:k] = [Token('CODE', 'shlex.join'), Token('OP', '(')] 28 | 29 | 30 | @register(ast.Call) 31 | def visit_Call( 32 | state: State, 33 | node: ast.Call, 34 | parent: ast.AST, 35 | ) -> Iterable[tuple[Offset, TokenFunc]]: 36 | if state.settings.min_version < (3, 8): 37 | return 38 | 39 | if ( 40 | isinstance(node.func, ast.Attribute) and 41 | isinstance(node.func.value, ast.Constant) and 42 | node.func.value.value == ' ' and 43 | node.func.attr == 'join' and 44 | not node.keywords and 45 | len(node.args) == 1 and 46 | isinstance(node.args[0], (ast.ListComp, ast.GeneratorExp)) and 47 | isinstance(node.args[0].elt, ast.Call) and 48 | isinstance(node.args[0].elt.func, ast.Attribute) and 49 | isinstance(node.args[0].elt.func.value, ast.Name) and 50 | node.args[0].elt.func.value.id == 'shlex' and 51 | node.args[0].elt.func.attr == 'quote' and 52 | not node.args[0].elt.keywords and 53 | len(node.args[0].elt.args) == 1 and 54 | isinstance(node.args[0].elt.args[0], ast.Name) and 55 | len(node.args[0].generators) == 1 and 56 | isinstance(node.args[0].generators[0].target, ast.Name) and 57 | not node.args[0].generators[0].ifs and 58 | not node.args[0].generators[0].is_async and 59 | node.args[0].elt.args[0].id == node.args[0].generators[0].target.id 60 | ): 61 | func = functools.partial(_fix_shlex_join, arg=node.args[0]) 62 | yield ast_to_offset(node), func 63 | -------------------------------------------------------------------------------- /pyupgrade/_plugins/six_base_classes.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import ast 4 | from collections.abc import Iterable 5 | 6 | from tokenize_rt import Offset 7 | 8 | from pyupgrade._ast_helpers import ast_to_offset 9 | from pyupgrade._ast_helpers import is_name_attr 10 | from pyupgrade._data import register 11 | from pyupgrade._data import State 12 | from pyupgrade._data import TokenFunc 13 | from pyupgrade._token_helpers import remove_base_class 14 | 15 | 16 | @register(ast.ClassDef) 17 | def visit_ClassDef( 18 | state: State, 19 | node: ast.ClassDef, 20 | parent: ast.AST, 21 | ) -> Iterable[tuple[Offset, TokenFunc]]: 22 | for base in node.bases: 23 | if is_name_attr(base, state.from_imports, ('six',), ('Iterator',)): 24 | yield ast_to_offset(base), remove_base_class 25 | -------------------------------------------------------------------------------- /pyupgrade/_plugins/six_calls.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import ast 4 | import functools 5 | from collections.abc import Iterable 6 | 7 | from tokenize_rt import Offset 8 | from tokenize_rt import Token 9 | 10 | from pyupgrade._ast_helpers import ast_to_offset 11 | from pyupgrade._ast_helpers import has_starargs 12 | from pyupgrade._ast_helpers import is_name_attr 13 | from pyupgrade._data import register 14 | from pyupgrade._data import State 15 | from pyupgrade._data import TokenFunc 16 | from pyupgrade._token_helpers import find_and_replace_call 17 | from pyupgrade._token_helpers import find_op 18 | from pyupgrade._token_helpers import parse_call_args 19 | from pyupgrade._token_helpers import replace_call 20 | 21 | _EXPR_NEEDS_PARENS: tuple[type[ast.expr], ...] = ( 22 | ast.Await, ast.BinOp, ast.BoolOp, ast.Compare, ast.GeneratorExp, ast.IfExp, 23 | ast.Lambda, ast.UnaryOp, ast.NamedExpr, 24 | ) 25 | 26 | SIX_CALLS = { 27 | 'u': '{args[0]}', 28 | 'byte2int': '{args[0]}[0]', 29 | 'indexbytes': '{args[0]}[{rest}]', 30 | 'iteritems': '{args[0]}.items()', 31 | 'iterkeys': '{args[0]}.keys()', 32 | 'itervalues': '{args[0]}.values()', 33 | 'viewitems': '{args[0]}.items()', 34 | 'viewkeys': '{args[0]}.keys()', 35 | 'viewvalues': '{args[0]}.values()', 36 | 'create_unbound_method': '{args[0]}', 37 | 'get_unbound_function': '{args[0]}', 38 | 'get_method_function': '{args[0]}.__func__', 39 | 'get_method_self': '{args[0]}.__self__', 40 | 'get_function_closure': '{args[0]}.__closure__', 41 | 'get_function_code': '{args[0]}.__code__', 42 | 'get_function_defaults': '{args[0]}.__defaults__', 43 | 'get_function_globals': '{args[0]}.__globals__', 44 | 'assertCountEqual': '{args[0]}.assertCountEqual({rest})', 45 | 'assertRaisesRegex': '{args[0]}.assertRaisesRegex({rest})', 46 | 'assertRegex': '{args[0]}.assertRegex({rest})', 47 | } 48 | SIX_INT2BYTE_TMPL = 'bytes(({args[0]},))' 49 | RAISE_FROM_TMPL = 'raise {args[0]} from {args[1]}' 50 | RERAISE_TMPL = 'raise' 51 | RERAISE_2_TMPL = 'raise {args[1]}.with_traceback(None)' 52 | RERAISE_3_TMPL = 'raise {args[1]}.with_traceback({args[2]})' 53 | 54 | 55 | def _fix_six_b(i: int, tokens: list[Token]) -> None: 56 | j = find_op(tokens, i, '(') 57 | if ( 58 | tokens[j + 1].name == 'STRING' and 59 | tokens[j + 1].src.isascii() and 60 | tokens[j + 2].src == ')' 61 | ): 62 | func_args, end = parse_call_args(tokens, j) 63 | replace_call(tokens, i, end, func_args, 'b{args[0]}') 64 | 65 | 66 | @register(ast.Call) 67 | def visit_Call( 68 | state: State, 69 | node: ast.Call, 70 | parent: ast.AST, 71 | ) -> Iterable[tuple[Offset, TokenFunc]]: 72 | if isinstance(node.func, ast.Name): 73 | name = node.func.id 74 | elif isinstance(node.func, ast.Attribute): 75 | name = node.func.attr 76 | else: 77 | return 78 | 79 | if ( 80 | is_name_attr( 81 | node.func, 82 | state.from_imports, 83 | ('six',), 84 | ('iteritems', 'iterkeys', 'itervalues'), 85 | ) and 86 | node.args and 87 | not has_starargs(node) and 88 | # parent is next(...) 89 | isinstance(parent, ast.Call) and 90 | isinstance(parent.func, ast.Name) and 91 | parent.func.id == 'next' 92 | ): 93 | func = functools.partial( 94 | find_and_replace_call, 95 | template=f'iter({SIX_CALLS[name]})', 96 | ) 97 | yield ast_to_offset(node), func 98 | elif ( 99 | is_name_attr( 100 | node.func, 101 | state.from_imports, 102 | ('six',), 103 | SIX_CALLS, 104 | ) and 105 | node.args and 106 | not has_starargs(node) 107 | ): 108 | if isinstance(node.args[0], _EXPR_NEEDS_PARENS): 109 | parens: tuple[int, ...] = (0,) 110 | else: 111 | parens = () 112 | func = functools.partial( 113 | find_and_replace_call, 114 | template=SIX_CALLS[name], 115 | parens=parens, 116 | ) 117 | yield ast_to_offset(node), func 118 | elif ( 119 | is_name_attr( 120 | node.func, 121 | state.from_imports, 122 | ('six',), 123 | ('int2byte',), 124 | ) and 125 | node.args and 126 | not has_starargs(node) 127 | ): 128 | func = functools.partial( 129 | find_and_replace_call, 130 | template=SIX_INT2BYTE_TMPL, 131 | ) 132 | yield ast_to_offset(node), func 133 | elif ( 134 | is_name_attr( 135 | node.func, 136 | state.from_imports, 137 | ('six',), 138 | ('b', 'ensure_binary'), 139 | ) and 140 | not node.keywords and 141 | not has_starargs(node) and 142 | len(node.args) == 1 and 143 | isinstance(node.args[0], ast.Constant) and 144 | isinstance(node.args[0].value, str) 145 | ): 146 | yield ast_to_offset(node), _fix_six_b 147 | elif ( 148 | isinstance(parent, ast.Expr) and 149 | is_name_attr( 150 | node.func, 151 | state.from_imports, 152 | ('six',), 153 | ('raise_from',), 154 | ) and 155 | node.args and 156 | not has_starargs(node) 157 | ): 158 | func = functools.partial( 159 | find_and_replace_call, 160 | template=RAISE_FROM_TMPL, 161 | ) 162 | yield ast_to_offset(node), func 163 | elif ( 164 | isinstance(parent, ast.Expr) and 165 | is_name_attr( 166 | node.func, 167 | state.from_imports, 168 | ('six',), 169 | ('reraise',), 170 | ) 171 | ): 172 | if ( 173 | len(node.args) == 2 and 174 | not node.keywords and 175 | not has_starargs(node) 176 | ): 177 | func = functools.partial( 178 | find_and_replace_call, 179 | template=RERAISE_2_TMPL, 180 | ) 181 | yield ast_to_offset(node), func 182 | elif ( 183 | len(node.args) == 3 and 184 | not node.keywords and 185 | not has_starargs(node) 186 | ): 187 | func = functools.partial( 188 | find_and_replace_call, 189 | template=RERAISE_3_TMPL, 190 | ) 191 | yield ast_to_offset(node), func 192 | elif ( 193 | len(node.args) == 1 and 194 | not node.keywords and 195 | isinstance(node.args[0], ast.Starred) and 196 | isinstance(node.args[0].value, ast.Call) and 197 | is_name_attr( 198 | node.args[0].value.func, 199 | state.from_imports, 200 | ('sys',), 201 | ('exc_info',), 202 | ) 203 | ): 204 | func = functools.partial( 205 | find_and_replace_call, 206 | template=RERAISE_TMPL, 207 | ) 208 | yield ast_to_offset(node), func 209 | -------------------------------------------------------------------------------- /pyupgrade/_plugins/six_metaclasses.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import ast 4 | from collections.abc import Iterable 5 | 6 | from tokenize_rt import NON_CODING_TOKENS 7 | from tokenize_rt import Offset 8 | from tokenize_rt import Token 9 | 10 | from pyupgrade._ast_helpers import ast_to_offset 11 | from pyupgrade._ast_helpers import has_starargs 12 | from pyupgrade._ast_helpers import is_name_attr 13 | from pyupgrade._data import register 14 | from pyupgrade._data import State 15 | from pyupgrade._data import TokenFunc 16 | from pyupgrade._token_helpers import arg_str 17 | from pyupgrade._token_helpers import find_block_start 18 | from pyupgrade._token_helpers import find_op 19 | from pyupgrade._token_helpers import parse_call_args 20 | from pyupgrade._token_helpers import remove_decorator 21 | from pyupgrade._token_helpers import replace_call 22 | 23 | 24 | def _fix_add_metaclass(i: int, tokens: list[Token]) -> None: 25 | j = find_op(tokens, i, '(') 26 | func_args, end = parse_call_args(tokens, j) 27 | metaclass = f'metaclass={arg_str(tokens, *func_args[0])}' 28 | # insert `metaclass={args[0]}` into `class:` 29 | # search forward for the `class` token 30 | j = i + 1 31 | while not tokens[j].matches(name='NAME', src='class'): 32 | j += 1 33 | class_token = j 34 | # then search forward for a `:` token, not inside a brace 35 | j = find_block_start(tokens, j) 36 | last_paren = -1 37 | for k in range(class_token, j): 38 | if tokens[k].src == ')': 39 | last_paren = k 40 | 41 | if last_paren == -1: 42 | tokens.insert(j, Token('CODE', f'({metaclass})')) 43 | else: 44 | insert = last_paren - 1 45 | while tokens[insert].name in NON_CODING_TOKENS: 46 | insert -= 1 47 | if tokens[insert].src == '(': # no bases 48 | src = metaclass 49 | elif tokens[insert].src != ',': 50 | src = f', {metaclass}' 51 | else: 52 | src = f' {metaclass},' 53 | tokens.insert(insert + 1, Token('CODE', src)) 54 | remove_decorator(i, tokens) 55 | 56 | 57 | def _fix_with_metaclass(i: int, tokens: list[Token]) -> None: 58 | j = find_op(tokens, i, '(') 59 | func_args, end = parse_call_args(tokens, j) 60 | if len(func_args) == 1: 61 | tmpl = 'metaclass={args[0]}' 62 | elif len(func_args) == 2: 63 | base = arg_str(tokens, *func_args[1]) 64 | if base == 'object': 65 | tmpl = 'metaclass={args[0]}' 66 | else: 67 | tmpl = '{rest}, metaclass={args[0]}' 68 | else: 69 | tmpl = '{rest}, metaclass={args[0]}' 70 | replace_call(tokens, i, end, func_args, tmpl) 71 | 72 | 73 | @register(ast.ClassDef) 74 | def visit_ClassDef( 75 | state: State, 76 | node: ast.ClassDef, 77 | parent: ast.AST, 78 | ) -> Iterable[tuple[Offset, TokenFunc]]: 79 | for decorator in node.decorator_list: 80 | if ( 81 | isinstance(decorator, ast.Call) and 82 | is_name_attr( 83 | decorator.func, 84 | state.from_imports, 85 | ('six',), 86 | ('add_metaclass',), 87 | ) and 88 | not has_starargs(decorator) 89 | ): 90 | yield ast_to_offset(decorator), _fix_add_metaclass 91 | 92 | if ( 93 | len(node.bases) == 1 and 94 | isinstance(node.bases[0], ast.Call) and 95 | is_name_attr( 96 | node.bases[0].func, 97 | state.from_imports, 98 | ('six',), 99 | ('with_metaclass',), 100 | ) and 101 | not has_starargs(node.bases[0]) 102 | ): 103 | yield ast_to_offset(node.bases[0]), _fix_with_metaclass 104 | -------------------------------------------------------------------------------- /pyupgrade/_plugins/six_remove_decorators.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import ast 4 | from collections.abc import Iterable 5 | 6 | from tokenize_rt import Offset 7 | 8 | from pyupgrade._ast_helpers import ast_to_offset 9 | from pyupgrade._ast_helpers import is_name_attr 10 | from pyupgrade._data import register 11 | from pyupgrade._data import State 12 | from pyupgrade._data import TokenFunc 13 | from pyupgrade._token_helpers import remove_decorator 14 | 15 | 16 | @register(ast.ClassDef) 17 | def visit_ClassDef( 18 | state: State, 19 | node: ast.ClassDef, 20 | parent: ast.AST, 21 | ) -> Iterable[tuple[Offset, TokenFunc]]: 22 | for decorator in node.decorator_list: 23 | if is_name_attr( 24 | decorator, 25 | state.from_imports, 26 | ('six',), 27 | ('python_2_unicode_compatible',), 28 | ): 29 | yield ast_to_offset(decorator), remove_decorator 30 | -------------------------------------------------------------------------------- /pyupgrade/_plugins/six_simple.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import ast 4 | import functools 5 | from collections.abc import Iterable 6 | 7 | from tokenize_rt import Offset 8 | 9 | from pyupgrade._ast_helpers import ast_to_offset 10 | from pyupgrade._ast_helpers import is_type_check 11 | from pyupgrade._data import register 12 | from pyupgrade._data import State 13 | from pyupgrade._data import TokenFunc 14 | from pyupgrade._plugins.imports import REMOVALS 15 | from pyupgrade._plugins.native_literals import is_a_native_literal_call 16 | from pyupgrade._token_helpers import replace_name 17 | 18 | NAMES = { 19 | 'text_type': 'str', 20 | 'binary_type': 'bytes', 21 | 'class_types': '(type,)', 22 | 'string_types': '(str,)', 23 | 'integer_types': '(int,)', 24 | 'unichr': 'chr', 25 | 'iterbytes': 'iter', 26 | 'print_': 'print', 27 | 'exec_': 'exec', 28 | 'advance_iterator': 'next', 29 | 'next': 'next', 30 | 'callable': 'callable', 31 | } 32 | NAMES_MOVES = REMOVALS[(3,)]['six.moves'] 33 | NAMES_TYPE_CTX = { 34 | 'class_types': 'type', 35 | 'string_types': 'str', 36 | 'integer_types': 'int', 37 | } 38 | 39 | 40 | @register(ast.Attribute) 41 | def visit_Attribute( 42 | state: State, 43 | node: ast.Attribute, 44 | parent: ast.AST, 45 | ) -> Iterable[tuple[Offset, TokenFunc]]: 46 | if ( 47 | isinstance(node.value, ast.Name) and 48 | node.value.id == 'six' and 49 | node.attr in NAMES 50 | ): 51 | # these will be handled by the native literals plugin 52 | if ( 53 | isinstance(parent, ast.Call) and 54 | is_a_native_literal_call(parent, state.from_imports) 55 | ): 56 | return 57 | 58 | if node.attr in NAMES_TYPE_CTX and is_type_check(parent): 59 | new = NAMES_TYPE_CTX[node.attr] 60 | else: 61 | new = NAMES[node.attr] 62 | 63 | func = functools.partial(replace_name, name=node.attr, new=new) 64 | yield ast_to_offset(node), func 65 | elif ( 66 | isinstance(node.value, ast.Attribute) and 67 | isinstance(node.value.value, ast.Name) and 68 | node.value.value.id == 'six' and 69 | node.value.attr == 'moves' and 70 | node.attr == 'xrange' 71 | ): 72 | func = functools.partial(replace_name, name=node.attr, new='range') 73 | yield ast_to_offset(node), func 74 | elif ( 75 | isinstance(node.value, ast.Attribute) and 76 | isinstance(node.value.value, ast.Name) and 77 | node.value.value.id == 'six' and 78 | node.value.attr == 'moves' and 79 | node.attr in NAMES_MOVES 80 | ): 81 | func = functools.partial(replace_name, name=node.attr, new=node.attr) 82 | yield ast_to_offset(node), func 83 | 84 | 85 | @register(ast.Name) 86 | def visit_Name( 87 | state: State, 88 | node: ast.Name, 89 | parent: ast.AST, 90 | ) -> Iterable[tuple[Offset, TokenFunc]]: 91 | if ( 92 | node.id in state.from_imports['six'] and 93 | node.id in NAMES 94 | ): 95 | # these will be handled by the native literals plugin 96 | if ( 97 | isinstance(parent, ast.Call) and 98 | is_a_native_literal_call(parent, state.from_imports) 99 | ): 100 | return 101 | 102 | if node.id in NAMES_TYPE_CTX and is_type_check(parent): 103 | new = NAMES_TYPE_CTX[node.id] 104 | else: 105 | new = NAMES[node.id] 106 | 107 | func = functools.partial(replace_name, name=node.id, new=new) 108 | yield ast_to_offset(node), func 109 | elif ( 110 | node.id in state.from_imports['six.moves'] and 111 | node.id in {'xrange', 'range'} 112 | ): 113 | func = functools.partial(replace_name, name=node.id, new='range') 114 | yield ast_to_offset(node), func 115 | -------------------------------------------------------------------------------- /pyupgrade/_plugins/subprocess_run.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import ast 4 | import functools 5 | from collections.abc import Iterable 6 | 7 | from tokenize_rt import Offset 8 | from tokenize_rt import Token 9 | 10 | from pyupgrade._ast_helpers import ast_to_offset 11 | from pyupgrade._ast_helpers import is_name_attr 12 | from pyupgrade._data import register 13 | from pyupgrade._data import State 14 | from pyupgrade._data import TokenFunc 15 | from pyupgrade._token_helpers import delete_argument 16 | from pyupgrade._token_helpers import find_op 17 | from pyupgrade._token_helpers import parse_call_args 18 | from pyupgrade._token_helpers import replace_argument 19 | 20 | 21 | def _use_capture_output( 22 | i: int, 23 | tokens: list[Token], 24 | *, 25 | stdout_arg_idx: int, 26 | stderr_arg_idx: int, 27 | ) -> None: 28 | j = find_op(tokens, i, '(') 29 | func_args, _ = parse_call_args(tokens, j) 30 | if stdout_arg_idx < stderr_arg_idx: 31 | delete_argument(stderr_arg_idx, tokens, func_args) 32 | replace_argument( 33 | stdout_arg_idx, 34 | tokens, 35 | func_args, 36 | new='capture_output=True', 37 | ) 38 | else: 39 | replace_argument( 40 | stdout_arg_idx, 41 | tokens, 42 | func_args, 43 | new='capture_output=True', 44 | ) 45 | delete_argument(stderr_arg_idx, tokens, func_args) 46 | 47 | 48 | def _replace_universal_newlines_with_text( 49 | i: int, 50 | tokens: list[Token], 51 | *, 52 | arg_idx: int, 53 | ) -> None: 54 | j = find_op(tokens, i, '(') 55 | func_args, _ = parse_call_args(tokens, j) 56 | for i in range(*func_args[arg_idx]): 57 | if tokens[i].src == 'universal_newlines': 58 | tokens[i] = tokens[i]._replace(src='text') 59 | break 60 | else: 61 | raise AssertionError('`universal_newlines` argument not found') 62 | 63 | 64 | @register(ast.Call) 65 | def visit_Call( 66 | state: State, 67 | node: ast.Call, 68 | parent: ast.AST, 69 | ) -> Iterable[tuple[Offset, TokenFunc]]: 70 | if ( 71 | state.settings.min_version >= (3, 7) and 72 | is_name_attr( 73 | node.func, 74 | state.from_imports, 75 | ('subprocess',), 76 | ('check_output', 'run'), 77 | ) 78 | ): 79 | stdout_idx = None 80 | stderr_idx = None 81 | universal_newlines_idx = None 82 | skip_universal_newlines_rewrite = False 83 | for n, keyword in enumerate(node.keywords): 84 | if keyword.arg == 'stdout' and is_name_attr( 85 | keyword.value, 86 | state.from_imports, 87 | ('subprocess',), 88 | ('PIPE',), 89 | ): 90 | stdout_idx = n 91 | elif keyword.arg == 'stderr' and is_name_attr( 92 | keyword.value, 93 | state.from_imports, 94 | ('subprocess',), 95 | ('PIPE',), 96 | ): 97 | stderr_idx = n 98 | elif keyword.arg == 'universal_newlines': 99 | universal_newlines_idx = n 100 | elif keyword.arg == 'text' or keyword.arg is None: 101 | skip_universal_newlines_rewrite = True 102 | if ( 103 | universal_newlines_idx is not None and 104 | not skip_universal_newlines_rewrite 105 | ): 106 | func = functools.partial( 107 | _replace_universal_newlines_with_text, 108 | arg_idx=len(node.args) + universal_newlines_idx, 109 | ) 110 | yield ast_to_offset(node), func 111 | if stdout_idx is not None and stderr_idx is not None: 112 | func = functools.partial( 113 | _use_capture_output, 114 | stdout_arg_idx=len(node.args) + stdout_idx, 115 | stderr_arg_idx=len(node.args) + stderr_idx, 116 | ) 117 | yield ast_to_offset(node), func 118 | -------------------------------------------------------------------------------- /pyupgrade/_plugins/type_of_primitive.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import ast 4 | import functools 5 | from collections.abc import Iterable 6 | 7 | from tokenize_rt import Offset 8 | from tokenize_rt import Token 9 | 10 | from pyupgrade._ast_helpers import ast_to_offset 11 | from pyupgrade._data import register 12 | from pyupgrade._data import State 13 | from pyupgrade._data import TokenFunc 14 | from pyupgrade._token_helpers import find_closing_bracket 15 | from pyupgrade._token_helpers import find_op 16 | 17 | _TYPES = { 18 | bool: 'bool', 19 | bytes: 'bytes', 20 | str: 'str', 21 | int: 'int', 22 | float: 'float', 23 | complex: 'complex', 24 | } 25 | 26 | 27 | def _rewrite_type_of_primitive( 28 | i: int, 29 | tokens: list[Token], 30 | *, 31 | src: str, 32 | ) -> None: 33 | open_paren = find_op(tokens, i + 1, '(') 34 | j = find_closing_bracket(tokens, open_paren) 35 | tokens[i] = tokens[i]._replace(src=src) 36 | del tokens[i + 1:j + 1] 37 | 38 | 39 | @register(ast.Call) 40 | def visit_Call( 41 | state: State, 42 | node: ast.Call, 43 | parent: ast.AST, 44 | ) -> Iterable[tuple[Offset, TokenFunc]]: 45 | if ( 46 | isinstance(node.func, ast.Name) and 47 | node.func.id == 'type' and 48 | len(node.args) == 1 and 49 | isinstance(node.args[0], ast.Constant) and 50 | node.args[0].value not in {Ellipsis, None} 51 | ): 52 | func = functools.partial( 53 | _rewrite_type_of_primitive, 54 | src=_TYPES[type(node.args[0].value)], 55 | ) 56 | yield ast_to_offset(node), func 57 | -------------------------------------------------------------------------------- /pyupgrade/_plugins/typing_pep563.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import ast 4 | import functools 5 | import sys 6 | from collections.abc import Iterable 7 | from collections.abc import Sequence 8 | 9 | from tokenize_rt import Offset 10 | 11 | from pyupgrade._ast_helpers import ast_to_offset 12 | from pyupgrade._data import register 13 | from pyupgrade._data import State 14 | from pyupgrade._data import Token 15 | from pyupgrade._data import TokenFunc 16 | 17 | 18 | def _supported_version(state: State) -> bool: 19 | return ( 20 | state.settings.min_version >= (3, 14) or 21 | 'annotations' in state.from_imports['__future__'] 22 | ) 23 | 24 | 25 | def _dequote(i: int, tokens: list[Token], *, new: str) -> None: 26 | tokens[i] = tokens[i]._replace(src=new) 27 | 28 | 29 | def _get_name(node: ast.expr) -> str: 30 | if isinstance(node, ast.Name): 31 | return node.id 32 | elif isinstance(node, ast.Attribute): 33 | return node.attr 34 | else: 35 | raise AssertionError(f'expected Name or Attribute: {ast.dump(node)}') 36 | 37 | 38 | def _get_keyword_value( 39 | keywords: list[ast.keyword], 40 | keyword: str, 41 | ) -> ast.expr | None: 42 | for kw in keywords: 43 | if kw.arg == keyword: 44 | return kw.value 45 | else: 46 | return None 47 | 48 | 49 | def _process_call(node: ast.Call) -> Iterable[ast.AST]: 50 | name = _get_name(node.func) 51 | args = node.args 52 | keywords = node.keywords 53 | if name == 'TypedDict': 54 | if keywords: 55 | for keyword in keywords: 56 | yield keyword.value 57 | elif len(args) != 2: # garbage 58 | pass 59 | elif isinstance(args[1], ast.Dict): 60 | yield from args[1].values 61 | else: 62 | raise AssertionError(f'expected ast.Dict: {ast.dump(args[1])}') 63 | elif name == 'NamedTuple': 64 | if len(args) == 2: 65 | fields: ast.expr | None = args[1] 66 | elif keywords: 67 | fields = _get_keyword_value(keywords, 'fields') 68 | else: # garbage 69 | fields = None 70 | 71 | if isinstance(fields, ast.List): 72 | for elt in fields.elts: 73 | if isinstance(elt, ast.Tuple) and len(elt.elts) == 2: 74 | yield elt.elts[1] 75 | elif fields is not None: 76 | raise AssertionError(f'expected ast.List: {ast.dump(fields)}') 77 | elif name in { 78 | 'Arg', 79 | 'DefaultArg', 80 | 'NamedArg', 81 | 'DefaultNamedArg', 82 | 'VarArg', 83 | 'KwArg', 84 | }: 85 | if args: 86 | yield args[0] 87 | else: 88 | keyword_value = _get_keyword_value(keywords, 'type') 89 | if keyword_value is not None: 90 | yield keyword_value 91 | 92 | 93 | def _process_subscript(node: ast.Subscript) -> Iterable[ast.AST]: 94 | name = _get_name(node.value) 95 | if name == 'Annotated': 96 | if isinstance(node.slice, ast.Tuple) and node.slice.elts: 97 | yield node.slice.elts[0] 98 | elif name != 'Literal': 99 | yield node.slice 100 | 101 | 102 | def _replace_string_literal( 103 | annotation: ast.expr, 104 | ) -> Iterable[tuple[Offset, TokenFunc]]: 105 | nodes: list[ast.AST] = [annotation] 106 | while nodes: 107 | node = nodes.pop() 108 | if isinstance(node, ast.Call): 109 | nodes.extend(_process_call(node)) 110 | elif isinstance(node, ast.Subscript): 111 | nodes.extend(_process_subscript(node)) 112 | elif isinstance(node, ast.Constant) and isinstance(node.value, str): 113 | func = functools.partial(_dequote, new=node.value) 114 | yield ast_to_offset(node), func 115 | else: 116 | for name in node._fields: 117 | value = getattr(node, name) 118 | if isinstance(value, ast.AST): 119 | nodes.append(value) 120 | elif isinstance(value, list): 121 | nodes.extend(value) 122 | 123 | 124 | def _process_args( 125 | args: Sequence[ast.arg | None], 126 | ) -> Iterable[tuple[Offset, TokenFunc]]: 127 | for arg in args: 128 | if arg is not None and arg.annotation is not None: 129 | yield from _replace_string_literal(arg.annotation) 130 | 131 | 132 | def _visit_func( 133 | state: State, 134 | node: ast.AsyncFunctionDef | ast.FunctionDef, 135 | parent: ast.AST, 136 | ) -> Iterable[tuple[Offset, TokenFunc]]: 137 | if not _supported_version(state): 138 | return 139 | 140 | yield from _process_args([node.args.vararg, node.args.kwarg]) 141 | yield from _process_args(node.args.args) 142 | yield from _process_args(node.args.kwonlyargs) 143 | yield from _process_args(node.args.posonlyargs) 144 | if node.returns is not None: 145 | yield from _replace_string_literal(node.returns) 146 | 147 | 148 | register(ast.AsyncFunctionDef)(_visit_func) 149 | register(ast.FunctionDef)(_visit_func) 150 | 151 | 152 | @register(ast.AnnAssign) 153 | def visit_AnnAssign( 154 | state: State, 155 | node: ast.AnnAssign, 156 | parent: ast.AST, 157 | ) -> Iterable[tuple[Offset, TokenFunc]]: 158 | if not _supported_version(state): 159 | return 160 | yield from _replace_string_literal(node.annotation) 161 | 162 | 163 | if sys.version_info >= (3, 12): # pragma: >=3.12 cover 164 | @register(ast.TypeVar) 165 | def visit_TypeVar( 166 | state: State, 167 | node: ast.TypeVar, 168 | parent: ast.AST, 169 | ) -> Iterable[tuple[Offset, TokenFunc]]: 170 | if node.bound is not None: 171 | yield from _replace_string_literal(node.bound) 172 | -------------------------------------------------------------------------------- /pyupgrade/_plugins/typing_pep585.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import ast 4 | import functools 5 | from collections.abc import Iterable 6 | 7 | from tokenize_rt import Offset 8 | 9 | from pyupgrade._ast_helpers import ast_to_offset 10 | from pyupgrade._data import register 11 | from pyupgrade._data import State 12 | from pyupgrade._data import TokenFunc 13 | from pyupgrade._token_helpers import replace_name 14 | 15 | PEP585_BUILTINS = frozenset(( 16 | 'Dict', 'FrozenSet', 'List', 'Set', 'Tuple', 'Type', 17 | )) 18 | 19 | 20 | def _should_rewrite(state: State) -> bool: 21 | return ( 22 | state.settings.min_version >= (3, 9) or ( 23 | not state.settings.keep_runtime_typing and 24 | state.in_annotation and 25 | 'annotations' in state.from_imports['__future__'] 26 | ) 27 | ) 28 | 29 | 30 | @register(ast.Attribute) 31 | def visit_Attribute( 32 | state: State, 33 | node: ast.Attribute, 34 | parent: ast.AST, 35 | ) -> Iterable[tuple[Offset, TokenFunc]]: 36 | if ( 37 | _should_rewrite(state) and 38 | isinstance(node.value, ast.Name) and 39 | node.value.id == 'typing' and 40 | node.attr in PEP585_BUILTINS 41 | ): 42 | func = functools.partial( 43 | replace_name, 44 | name=node.attr, 45 | new=node.attr.lower(), 46 | ) 47 | yield ast_to_offset(node), func 48 | 49 | 50 | @register(ast.Name) 51 | def visit_Name( 52 | state: State, 53 | node: ast.Name, 54 | parent: ast.AST, 55 | ) -> Iterable[tuple[Offset, TokenFunc]]: 56 | if ( 57 | _should_rewrite(state) and 58 | node.id in state.from_imports['typing'] and 59 | node.id in PEP585_BUILTINS 60 | ): 61 | func = functools.partial( 62 | replace_name, 63 | name=node.id, 64 | new=node.id.lower(), 65 | ) 66 | yield ast_to_offset(node), func 67 | -------------------------------------------------------------------------------- /pyupgrade/_plugins/typing_pep604.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import ast 4 | import functools 5 | import sys 6 | from collections.abc import Iterable 7 | 8 | from tokenize_rt import NON_CODING_TOKENS 9 | from tokenize_rt import Offset 10 | from tokenize_rt import Token 11 | 12 | from pyupgrade._ast_helpers import ast_to_offset 13 | from pyupgrade._ast_helpers import is_name_attr 14 | from pyupgrade._data import register 15 | from pyupgrade._data import State 16 | from pyupgrade._data import TokenFunc 17 | from pyupgrade._token_helpers import find_closing_bracket 18 | from pyupgrade._token_helpers import find_op 19 | from pyupgrade._token_helpers import is_close 20 | from pyupgrade._token_helpers import is_open 21 | 22 | 23 | def _fix_optional(i: int, tokens: list[Token]) -> None: 24 | j = find_op(tokens, i, '[') 25 | k = find_closing_bracket(tokens, j) 26 | if tokens[j].line == tokens[k].line: 27 | tokens[k] = Token('CODE', ' | None') 28 | del tokens[i:j + 1] 29 | else: 30 | tokens[j] = tokens[j]._replace(src='(') 31 | tokens[k] = tokens[k]._replace(src=')') 32 | tokens[i:j] = [Token('CODE', 'None | ')] 33 | 34 | 35 | def _fix_union( 36 | i: int, 37 | tokens: list[Token], 38 | *, 39 | arg_count: int, 40 | ) -> None: 41 | depth = 1 42 | parens_done = [] 43 | open_parens = [] 44 | commas = [] 45 | coding_depth = None 46 | 47 | j = find_op(tokens, i, '[') 48 | k = j + 1 49 | while depth: 50 | # it's possible our first coding token is a close paren 51 | # so make sure this is separate from the if chain below 52 | if ( 53 | tokens[k].name not in NON_CODING_TOKENS and 54 | tokens[k].src != '(' and 55 | coding_depth is None 56 | ): 57 | if tokens[k].src == ')': # the coding token was an empty tuple 58 | coding_depth = depth - 1 59 | else: 60 | coding_depth = depth 61 | 62 | if is_open(tokens[k]): 63 | if tokens[k].src == '(': 64 | open_parens.append((depth, k)) 65 | 66 | depth += 1 67 | elif is_close(tokens[k]): 68 | if tokens[k].src == ')': 69 | paren_depth, open_paren = open_parens.pop() 70 | parens_done.append((paren_depth, (open_paren, k))) 71 | 72 | depth -= 1 73 | elif tokens[k].src == ',': 74 | commas.append((depth, k)) 75 | 76 | k += 1 77 | k -= 1 78 | 79 | assert coding_depth is not None 80 | assert not open_parens, open_parens 81 | comma_depth = min((depth for depth, _ in commas), default=sys.maxsize) 82 | min_depth = min(comma_depth, coding_depth) 83 | 84 | to_delete = [ 85 | paren 86 | for depth, positions in parens_done 87 | if depth < min_depth 88 | for paren in positions 89 | ] 90 | 91 | if comma_depth <= coding_depth: 92 | comma_positions = [k for depth, k in commas if depth == comma_depth] 93 | if len(comma_positions) == arg_count: 94 | to_delete.append(comma_positions.pop()) 95 | else: 96 | comma_positions = [] 97 | 98 | to_delete.sort() 99 | 100 | if tokens[j].line == tokens[k].line: 101 | del tokens[k] 102 | for comma in comma_positions: 103 | tokens[comma] = Token('CODE', ' |') 104 | for paren in reversed(to_delete): 105 | del tokens[paren] 106 | del tokens[i:j + 1] 107 | else: 108 | tokens[j] = tokens[j]._replace(src='(') 109 | tokens[k] = tokens[k]._replace(src=')') 110 | 111 | for comma in comma_positions: 112 | tokens[comma] = Token('CODE', ' |') 113 | for paren in reversed(to_delete): 114 | del tokens[paren] 115 | del tokens[i:j] 116 | 117 | 118 | def _supported_version(state: State) -> bool: 119 | return ( 120 | state.in_annotation and ( 121 | state.settings.min_version >= (3, 10) or ( 122 | not state.settings.keep_runtime_typing and 123 | 'annotations' in state.from_imports['__future__'] 124 | ) 125 | ) 126 | ) 127 | 128 | 129 | def _any_arg_is_str(node_slice: ast.expr) -> bool: 130 | return ( 131 | ( 132 | isinstance(node_slice, ast.Constant) and 133 | isinstance(node_slice.value, str) 134 | ) or ( 135 | isinstance(node_slice, ast.Tuple) and 136 | any( 137 | isinstance(elt, ast.Constant) and 138 | isinstance(elt.value, str) 139 | for elt in node_slice.elts 140 | ) 141 | ) 142 | ) 143 | 144 | 145 | @register(ast.Subscript) 146 | def visit_Subscript( 147 | state: State, 148 | node: ast.Subscript, 149 | parent: ast.AST, 150 | ) -> Iterable[tuple[Offset, TokenFunc]]: 151 | if not _supported_version(state): 152 | return 153 | 154 | # don't rewrite forward annotations (unless we know they will be dequoted) 155 | if 'annotations' not in state.from_imports['__future__']: 156 | if _any_arg_is_str(node.slice): 157 | return 158 | 159 | if is_name_attr( 160 | node.value, 161 | state.from_imports, 162 | ('typing',), 163 | ('Optional',), 164 | ): 165 | yield ast_to_offset(node), _fix_optional 166 | elif is_name_attr(node.value, state.from_imports, ('typing',), ('Union',)): 167 | if isinstance(node.slice, ast.Slice): # not a valid annotation 168 | return 169 | 170 | if isinstance(node.slice, ast.Tuple): 171 | if node.slice.elts: 172 | arg_count = len(node.slice.elts) 173 | else: 174 | return # empty Union 175 | else: 176 | arg_count = 1 177 | 178 | func = functools.partial(_fix_union, arg_count=arg_count) 179 | yield ast_to_offset(node), func 180 | -------------------------------------------------------------------------------- /pyupgrade/_plugins/typing_pep646_unpack.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import ast 4 | from collections.abc import Iterable 5 | 6 | from tokenize_rt import Offset 7 | from tokenize_rt import Token 8 | 9 | from pyupgrade._ast_helpers import ast_to_offset 10 | from pyupgrade._ast_helpers import is_name_attr 11 | from pyupgrade._data import register 12 | from pyupgrade._data import State 13 | from pyupgrade._data import TokenFunc 14 | from pyupgrade._token_helpers import find_closing_bracket 15 | from pyupgrade._token_helpers import find_op 16 | from pyupgrade._token_helpers import remove_brace 17 | 18 | 19 | def _replace_unpack_with_star(i: int, tokens: list[Token]) -> None: 20 | start = find_op(tokens, i, '[') 21 | end = find_closing_bracket(tokens, start) 22 | 23 | remove_brace(tokens, end) 24 | # replace `Unpack` with `*` 25 | tokens[i:start + 1] = [tokens[i]._replace(name='OP', src='*')] 26 | 27 | 28 | @register(ast.Subscript) 29 | def visit_Subscript( 30 | state: State, 31 | node: ast.Subscript, 32 | parent: ast.AST, 33 | ) -> Iterable[tuple[Offset, TokenFunc]]: 34 | if state.settings.min_version < (3, 11): 35 | return 36 | 37 | if is_name_attr(node.value, state.from_imports, ('typing',), ('Unpack',)): 38 | if isinstance(parent, ast.Subscript): 39 | yield ast_to_offset(node.value), _replace_unpack_with_star 40 | 41 | 42 | def _visit_func( 43 | state: State, 44 | node: ast.AsyncFunctionDef | ast.FunctionDef, 45 | parent: ast.AST, 46 | ) -> Iterable[tuple[Offset, TokenFunc]]: 47 | if state.settings.min_version < (3, 11): 48 | return 49 | 50 | vararg = node.args.vararg 51 | if ( 52 | vararg is not None and 53 | isinstance(vararg.annotation, ast.Subscript) and 54 | is_name_attr( 55 | vararg.annotation.value, 56 | state.from_imports, 57 | ('typing',), ('Unpack',), 58 | ) 59 | ): 60 | yield ast_to_offset(vararg.annotation.value), _replace_unpack_with_star 61 | 62 | 63 | @register(ast.AsyncFunctionDef) 64 | def visit_AsyncFunctionDef( 65 | state: State, 66 | node: ast.AsyncFunctionDef, 67 | parent: ast.AST, 68 | ) -> Iterable[tuple[Offset, TokenFunc]]: 69 | yield from _visit_func(state, node, parent) 70 | 71 | 72 | @register(ast.FunctionDef) 73 | def visit_FunctionDef( 74 | state: State, 75 | node: ast.FunctionDef, 76 | parent: ast.AST, 77 | ) -> Iterable[tuple[Offset, TokenFunc]]: 78 | yield from _visit_func(state, node, parent) 79 | -------------------------------------------------------------------------------- /pyupgrade/_plugins/typing_pep696_typevar_defaults.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import ast 4 | from collections.abc import Iterable 5 | 6 | from tokenize_rt import Offset 7 | from tokenize_rt import Token 8 | 9 | from pyupgrade._ast_helpers import ast_to_offset 10 | from pyupgrade._ast_helpers import is_name_attr 11 | from pyupgrade._data import register 12 | from pyupgrade._data import State 13 | from pyupgrade._data import TokenFunc 14 | from pyupgrade._token_helpers import find_op 15 | from pyupgrade._token_helpers import parse_call_args 16 | 17 | 18 | def _fix_typevar_default(i: int, tokens: list[Token]) -> None: 19 | j = find_op(tokens, i, '[') 20 | args, end = parse_call_args(tokens, j) 21 | # remove the trailing `None` arguments 22 | del tokens[args[0][1]:args[-1][1]] 23 | 24 | 25 | def _should_rewrite(state: State) -> bool: 26 | return ( 27 | state.settings.min_version >= (3, 13) or ( 28 | not state.settings.keep_runtime_typing and 29 | state.in_annotation and 30 | 'annotations' in state.from_imports['__future__'] 31 | ) 32 | ) 33 | 34 | 35 | def _is_none(node: ast.AST) -> bool: 36 | return isinstance(node, ast.Constant) and node.value is None 37 | 38 | 39 | @register(ast.Subscript) 40 | def visit_Subscript( 41 | state: State, 42 | node: ast.Subscript, 43 | parent: ast.AST, 44 | ) -> Iterable[tuple[Offset, TokenFunc]]: 45 | if not _should_rewrite(state): 46 | return 47 | 48 | if ( 49 | is_name_attr( 50 | node.value, 51 | state.from_imports, 52 | ('collections.abc', 'typing', 'typing_extensions'), 53 | ('Generator',), 54 | ) and 55 | isinstance(node.slice, ast.Tuple) and 56 | len(node.slice.elts) == 3 and 57 | _is_none(node.slice.elts[1]) and 58 | _is_none(node.slice.elts[2]) 59 | ): 60 | yield ast_to_offset(node), _fix_typevar_default 61 | elif ( 62 | is_name_attr( 63 | node.value, 64 | state.from_imports, 65 | ('collections.abc', 'typing', 'typing_extensions'), 66 | ('AsyncGenerator',), 67 | ) and 68 | isinstance(node.slice, ast.Tuple) and 69 | len(node.slice.elts) == 2 and 70 | _is_none(node.slice.elts[1]) 71 | ): 72 | yield ast_to_offset(node), _fix_typevar_default 73 | -------------------------------------------------------------------------------- /pyupgrade/_plugins/typing_text.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import ast 4 | import functools 5 | from collections.abc import Iterable 6 | 7 | from tokenize_rt import Offset 8 | 9 | from pyupgrade._ast_helpers import ast_to_offset 10 | from pyupgrade._data import register 11 | from pyupgrade._data import State 12 | from pyupgrade._data import TokenFunc 13 | from pyupgrade._token_helpers import replace_name 14 | 15 | 16 | @register(ast.Attribute) 17 | def visit_Attribute( 18 | state: State, 19 | node: ast.Attribute, 20 | parent: ast.AST, 21 | ) -> Iterable[tuple[Offset, TokenFunc]]: 22 | if ( 23 | isinstance(node.value, ast.Name) and 24 | node.value.id == 'typing' and 25 | node.attr == 'Text' 26 | ): 27 | func = functools.partial(replace_name, name=node.attr, new='str') 28 | yield ast_to_offset(node), func 29 | 30 | 31 | @register(ast.Name) 32 | def visit_Name( 33 | state: State, 34 | node: ast.Name, 35 | parent: ast.AST, 36 | ) -> Iterable[tuple[Offset, TokenFunc]]: 37 | if node.id in state.from_imports['typing'] and node.id == 'Text': 38 | func = functools.partial(replace_name, name=node.id, new='str') 39 | yield ast_to_offset(node), func 40 | -------------------------------------------------------------------------------- /pyupgrade/_plugins/unittest_aliases.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import ast 4 | import functools 5 | from collections.abc import Iterable 6 | 7 | from tokenize_rt import Offset 8 | 9 | from pyupgrade._ast_helpers import ast_to_offset 10 | from pyupgrade._ast_helpers import has_starargs 11 | from pyupgrade._data import register 12 | from pyupgrade._data import State 13 | from pyupgrade._data import TokenFunc 14 | from pyupgrade._token_helpers import replace_name 15 | 16 | METHOD_MAPPING = { 17 | 'assertEquals': 'assertEqual', 18 | 'failUnlessEqual': 'assertEqual', 19 | 'failIfEqual': 'assertNotEqual', 20 | 'failUnless': 'assertTrue', 21 | 'assert_': 'assertTrue', 22 | 'failIf': 'assertFalse', 23 | 'failUnlessRaises': 'assertRaises', 24 | 'failUnlessAlmostEqual': 'assertAlmostEqual', 25 | 'failIfAlmostEqual': 'assertNotAlmostEqual', 26 | 'assertNotEquals': 'assertNotEqual', 27 | 'assertAlmostEquals': 'assertAlmostEqual', 28 | 'assertNotAlmostEquals': 'assertNotAlmostEqual', 29 | 'assertRegexpMatches': 'assertRegex', 30 | 'assertNotRegexpMatches': 'assertNotRegex', 31 | 'assertRaisesRegexp': 'assertRaisesRegex', 32 | } 33 | 34 | FUNCTION_MAPPING = { 35 | 'findTestCases': 'defaultTestLoader.loadTestsFromModule', 36 | 'makeSuite': 'defaultTestLoader.loadTestsFromTestCase', 37 | 'getTestCaseNames': 'defaultTestLoader.getTestCaseNames', 38 | } 39 | 40 | 41 | @register(ast.Call) 42 | def visit_Call( 43 | state: State, 44 | node: ast.Call, 45 | parent: ast.AST, 46 | ) -> Iterable[tuple[Offset, TokenFunc]]: 47 | if ( 48 | isinstance(node.func, ast.Attribute) and 49 | isinstance(node.func.value, ast.Name) and 50 | node.func.value.id == 'self' and 51 | node.func.attr in METHOD_MAPPING 52 | ): 53 | func = functools.partial( 54 | replace_name, 55 | name=node.func.attr, 56 | new=f'self.{METHOD_MAPPING[node.func.attr]}', 57 | ) 58 | yield ast_to_offset(node.func), func 59 | elif ( 60 | isinstance(node.func, ast.Attribute) and 61 | isinstance(node.func.value, ast.Name) and 62 | node.func.value.id == 'unittest' and 63 | node.func.attr in FUNCTION_MAPPING and 64 | not has_starargs(node) and 65 | not node.keywords and 66 | len(node.args) == 1 67 | ): 68 | func = functools.partial( 69 | replace_name, 70 | name=node.func.attr, 71 | new=f'unittest.{FUNCTION_MAPPING[node.func.attr]}', 72 | ) 73 | yield ast_to_offset(node.func), func 74 | -------------------------------------------------------------------------------- /pyupgrade/_plugins/unpack_list_comprehension.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import ast 4 | from collections.abc import Iterable 5 | 6 | from tokenize_rt import Offset 7 | from tokenize_rt import Token 8 | 9 | from pyupgrade._ast_helpers import ast_to_offset 10 | from pyupgrade._ast_helpers import is_async_listcomp 11 | from pyupgrade._data import register 12 | from pyupgrade._data import State 13 | from pyupgrade._data import TokenFunc 14 | from pyupgrade._token_helpers import find_closing_bracket 15 | 16 | 17 | def _replace_list_comprehension(i: int, tokens: list[Token]) -> None: 18 | start = i 19 | end = find_closing_bracket(tokens, start) 20 | tokens[start] = tokens[start]._replace(src='(') 21 | tokens[end] = tokens[end]._replace(src=')') 22 | 23 | 24 | @register(ast.Assign) 25 | def visit_Assign( 26 | state: State, 27 | node: ast.Assign, 28 | parent: ast.AST, 29 | ) -> Iterable[tuple[Offset, TokenFunc]]: 30 | if ( 31 | len(node.targets) == 1 and 32 | isinstance(node.targets[0], ast.Tuple) and 33 | isinstance(node.value, ast.ListComp) and 34 | not is_async_listcomp(node.value) 35 | ): 36 | yield ast_to_offset(node.value), _replace_list_comprehension 37 | -------------------------------------------------------------------------------- /pyupgrade/_plugins/versioned_branches.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import ast 4 | from collections.abc import Iterable 5 | from typing import cast 6 | 7 | from tokenize_rt import Offset 8 | from tokenize_rt import Token 9 | 10 | from pyupgrade._ast_helpers import ast_to_offset 11 | from pyupgrade._ast_helpers import is_name_attr 12 | from pyupgrade._data import register 13 | from pyupgrade._data import State 14 | from pyupgrade._data import TokenFunc 15 | from pyupgrade._data import Version 16 | from pyupgrade._token_helpers import Block 17 | 18 | 19 | def _find_if_else_block(tokens: list[Token], i: int) -> tuple[Block, Block]: 20 | if_block = Block.find(tokens, i) 21 | i = if_block.end 22 | while tokens[i].src != 'else': 23 | i += 1 24 | else_block = Block.find(tokens, i, trim_end=True) 25 | return if_block, else_block 26 | 27 | 28 | def _fix_py3_block(i: int, tokens: list[Token]) -> None: 29 | if tokens[i].src == 'if': 30 | if_block = Block.find(tokens, i) 31 | if_block.dedent(tokens) 32 | del tokens[if_block.start:if_block.block] 33 | else: 34 | if_block = Block.find(tokens, i) 35 | if_block.replace_condition(tokens, [Token('NAME', 'else')]) 36 | 37 | 38 | def _fix_py2_block(i: int, tokens: list[Token]) -> None: 39 | if tokens[i].src == 'if': 40 | if_block, else_block = _find_if_else_block(tokens, i) 41 | else_block.dedent(tokens) 42 | del tokens[if_block.start:else_block.block] 43 | else: 44 | if_block, else_block = _find_if_else_block(tokens, i) 45 | del tokens[if_block.start:else_block.start] 46 | 47 | 48 | def _fix_remove_block(i: int, tokens: list[Token]) -> None: 49 | block = Block.find(tokens, i) 50 | del tokens[block.start:block.end] 51 | 52 | 53 | def _fix_py2_convert_elif(i: int, tokens: list[Token]) -> None: 54 | if_block = Block.find(tokens, i) 55 | # wasn't actually followed by an `elif` 56 | if tokens[if_block.end].src != 'elif': 57 | return 58 | tokens[if_block.end] = Token('CODE', tokens[i].src) 59 | _fix_remove_block(i, tokens) 60 | 61 | 62 | def _fix_py3_block_else(i: int, tokens: list[Token]) -> None: 63 | if tokens[i].src == 'if': 64 | if_block, else_block = _find_if_else_block(tokens, i) 65 | if_block.dedent(tokens) 66 | del tokens[if_block.end:else_block.end] 67 | del tokens[if_block.start:if_block.block] 68 | else: 69 | if_block, else_block = _find_if_else_block(tokens, i) 70 | del tokens[if_block.end:else_block.end] 71 | if_block.replace_condition(tokens, [Token('NAME', 'else')]) 72 | 73 | 74 | def _fix_py3_convert_elif(i: int, tokens: list[Token]) -> None: 75 | if_block = Block.find(tokens, i) 76 | # wasn't actually followed by an `elif` 77 | if tokens[if_block.end].src != 'elif': 78 | return 79 | tokens[if_block.end] = Token('CODE', tokens[i].src) 80 | if_block.dedent(tokens) 81 | del tokens[if_block.start:if_block.block] 82 | 83 | 84 | def _eq(test: ast.Compare, n: int) -> bool: 85 | return ( 86 | isinstance(test.ops[0], ast.Eq) and 87 | isinstance(test.comparators[0], ast.Constant) and 88 | test.comparators[0].value == n 89 | ) 90 | 91 | 92 | def _compare_to_3( 93 | test: ast.Compare, 94 | op: type[ast.cmpop] | tuple[type[ast.cmpop], ...], 95 | minor: int = 0, 96 | ) -> bool: 97 | if not ( 98 | isinstance(test.ops[0], op) and 99 | isinstance(test.comparators[0], ast.Tuple) and 100 | len(test.comparators[0].elts) >= 1 and 101 | all( 102 | isinstance(n, ast.Constant) and isinstance(n.value, int) 103 | for n in test.comparators[0].elts 104 | ) 105 | ): 106 | return False 107 | 108 | # checked above but mypy needs help 109 | ast_elts = cast('list[ast.Constant]', test.comparators[0].elts) 110 | # padding a 0 for compatibility with (3,) used as a spec 111 | elts = tuple(e.value for e in ast_elts) + (0,) 112 | 113 | return elts[:2] == (3, minor) and all(n == 0 for n in elts[2:]) 114 | 115 | 116 | @register(ast.If) 117 | def visit_If( 118 | state: State, 119 | node: ast.If, 120 | parent: ast.AST, 121 | ) -> Iterable[tuple[Offset, TokenFunc]]: 122 | 123 | min_version: Version 124 | if state.settings.min_version == (3,): 125 | min_version = (3, 0) 126 | else: 127 | min_version = state.settings.min_version 128 | assert len(min_version) >= 2 129 | 130 | if ( 131 | # if six.PY2: 132 | is_name_attr( 133 | node.test, 134 | state.from_imports, 135 | ('six',), 136 | ('PY2',), 137 | ) or 138 | # if not six.PY3: 139 | ( 140 | isinstance(node.test, ast.UnaryOp) and 141 | isinstance(node.test.op, ast.Not) and 142 | is_name_attr( 143 | node.test.operand, 144 | state.from_imports, 145 | ('six',), 146 | ('PY3',), 147 | ) 148 | ) or 149 | # sys.version_info == 2 or < (3,) 150 | # or < (3, n) or <= (3, n) (with n= (3,) or > (3,) 195 | # sys.version_info >= (3, n) (with n<=m) 196 | # or sys.version_info > (3, n) (with n list[DotFormatPart]: 16 | """handle named escape sequences""" 17 | ret: list[DotFormatPart] = [] 18 | 19 | for part in NAMED_UNICODE_RE.split(s): 20 | if NAMED_UNICODE_RE.fullmatch(part): 21 | if not ret or ret[-1][1:] != (None, None, None): 22 | ret.append((part, None, None, None)) 23 | else: 24 | ret[-1] = (ret[-1][0] + part, None, None, None) 25 | else: 26 | first = True 27 | for tup in _stdlib_parse_format(part): 28 | if not first or not ret: 29 | ret.append(tup) 30 | else: 31 | ret[-1] = (ret[-1][0] + tup[0], *tup[1:]) 32 | first = False 33 | 34 | if not ret: 35 | ret.append((s, None, None, None)) 36 | 37 | return ret 38 | 39 | 40 | def unparse_parsed_string(parsed: list[DotFormatPart]) -> str: 41 | def _convert_tup(tup: DotFormatPart) -> str: 42 | ret, field_name, format_spec, conversion = tup 43 | ret = curly_escape(ret) 44 | if field_name is not None: 45 | ret += '{' + field_name 46 | if conversion: 47 | ret += '!' + conversion 48 | if format_spec: 49 | ret += ':' + format_spec 50 | ret += '}' 51 | return ret 52 | 53 | return ''.join(_convert_tup(tup) for tup in parsed) 54 | 55 | 56 | def is_codec(encoding: str, name: str) -> bool: 57 | try: 58 | return codecs.lookup(encoding).name == name 59 | except LookupError: 60 | return False 61 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | covdefaults>=2.1.0 2 | coverage 3 | pytest 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = pyupgrade 3 | version = 3.20.0 4 | description = A tool to automatically upgrade syntax for newer versions. 5 | long_description = file: README.md 6 | long_description_content_type = text/markdown 7 | url = https://github.com/asottile/pyupgrade 8 | author = Anthony Sottile 9 | author_email = asottile@umich.edu 10 | license = MIT 11 | license_files = LICENSE 12 | classifiers = 13 | Programming Language :: Python :: 3 14 | Programming Language :: Python :: 3 :: Only 15 | Programming Language :: Python :: Implementation :: CPython 16 | Programming Language :: Python :: Implementation :: PyPy 17 | 18 | [options] 19 | packages = find: 20 | install_requires = 21 | tokenize-rt>=6.1.0 22 | python_requires = >=3.9 23 | 24 | [options.packages.find] 25 | exclude = 26 | tests* 27 | testing* 28 | 29 | [options.entry_points] 30 | console_scripts = 31 | pyupgrade = pyupgrade._main:main 32 | 33 | [bdist_wheel] 34 | universal = True 35 | 36 | [coverage:run] 37 | plugins = covdefaults 38 | 39 | [mypy] 40 | check_untyped_defs = true 41 | disallow_any_generics = true 42 | disallow_incomplete_defs = true 43 | disallow_untyped_defs = true 44 | warn_redundant_casts = true 45 | warn_unused_ignores = true 46 | 47 | [mypy-testing.*] 48 | disallow_untyped_defs = false 49 | 50 | [mypy-tests.*] 51 | disallow_untyped_defs = false 52 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from setuptools import setup 4 | setup() 5 | -------------------------------------------------------------------------------- /testing/generate-imports: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from __future__ import annotations 3 | 4 | import collections 5 | import importlib.metadata 6 | import os.path 7 | import re 8 | import sys 9 | 10 | import reorder_python_imports 11 | 12 | FROM_IMPORT_RE = re.compile(r'^from ([^ ]+) import (.*)$') 13 | REPLACE_RE = re.compile(r'^([^=]+)=([^:]+)(?::(.*))?$') 14 | 15 | 16 | def _set_inline(v: set[str]) -> str: 17 | return f'{{{", ".join(repr(s) for s in sorted(v))}}}' 18 | 19 | 20 | def _dict_set_inline(ver: tuple[int, ...], dct: dict[str, set[str]]) -> str: 21 | items_s = ', '.join(f'{k!r}: {_set_inline(v)}' for k, v in dct.items()) 22 | return f' {ver!r}: {{{items_s}}},' 23 | 24 | 25 | def _set_fit(v: set[str]) -> str: 26 | ret = '' 27 | 28 | vals = sorted(v) 29 | pending = f'{" " * 12}{vals[0]!r},' 30 | 31 | for s in vals[1:]: 32 | if len(pending) + len(repr(s)) + 2 < 80: 33 | pending += f' {s!r},' 34 | else: 35 | ret += f'{pending}\n' 36 | pending = f'{" " * 12}{s!r},' 37 | 38 | ret += f'{pending}\n' 39 | 40 | return f'{{\n{ret}{" " * 8}}}' 41 | 42 | 43 | def _removals() -> dict[tuple[int, ...], dict[str, set[str]]]: 44 | removals: dict[tuple[int, ...], dict[str, set[str]]] 45 | removals = collections.defaultdict(lambda: collections.defaultdict(set)) 46 | for k, v in reorder_python_imports.REMOVALS.items(): 47 | if k <= (3, 6): 48 | k = (3,) 49 | for s in v: 50 | match = FROM_IMPORT_RE.match(s) 51 | assert match is not None 52 | removals[k][match[1]].add(match[2]) 53 | return removals 54 | 55 | 56 | def _replacements() -> tuple[ 57 | dict[tuple[int, ...], dict[tuple[str, str], str]], 58 | dict[str, str], 59 | ]: 60 | exact: dict[tuple[int, ...], dict[tuple[str, str], str]] 61 | exact = collections.defaultdict(dict) 62 | mods = {} 63 | 64 | for ver, vals in reorder_python_imports.REPLACES.items(): 65 | replaces = reorder_python_imports.Replacements.make([ 66 | reorder_python_imports._validate_replace_import(s) 67 | for s in vals 68 | if 'mock' not in s 69 | ]) 70 | if replaces.exact: 71 | exact[ver].update(replaces.exact) 72 | if replaces.mods: 73 | mods.update(replaces.mods) 74 | 75 | return exact, mods 76 | 77 | 78 | def main() -> int: 79 | version = importlib.metadata.version('reorder-python-imports') 80 | 81 | exact, mods = _replacements() 82 | 83 | print(f'# GENERATED VIA {os.path.basename(sys.argv[0])}') 84 | print(f'# Using reorder-python-imports=={version}') 85 | print('REMOVALS = {') 86 | for ver, dct in sorted(_removals().items()): 87 | dct_inline = _dict_set_inline(ver, dct) 88 | if len(dct_inline) < 80: 89 | print(dct_inline) 90 | else: 91 | print(f' {ver!r}: {{') 92 | for k, v in sorted(dct.items()): 93 | set_line = f' {k!r}: {_set_inline(v)},' 94 | if len(set_line) < 80: 95 | print(set_line) 96 | else: 97 | print(f' {k!r}: {_set_fit(v)},') 98 | print(' },') 99 | print('}') 100 | print("REMOVALS[(3,)]['six.moves.builtins'] = REMOVALS[(3,)]['builtins']") 101 | print('REPLACE_EXACT = {') 102 | for ver, replaces in sorted(exact.items()): 103 | print(f' {ver}: {{') 104 | for replace_k, replace_v in sorted(replaces.items()): 105 | print(f' {replace_k}: {replace_v!r},') 106 | print(' },') 107 | print('}') 108 | print('REPLACE_MODS = {') 109 | for mod_from, mod_to in sorted(mods.items()): 110 | print(f' {mod_from!r}: {mod_to!r},') 111 | print('}') 112 | print('# END GENERATED') 113 | return 0 114 | 115 | 116 | if __name__ == '__main__': 117 | raise SystemExit(main()) 118 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asottile/pyupgrade/1a0b8a1996416e51e374431887b12aae3f8d3e80/tests/__init__.py -------------------------------------------------------------------------------- /tests/_plugins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asottile/pyupgrade/1a0b8a1996416e51e374431887b12aae3f8d3e80/tests/_plugins/__init__.py -------------------------------------------------------------------------------- /tests/_plugins/typing_pep604_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | from tokenize_rt import src_to_tokens 5 | from tokenize_rt import tokens_to_src 6 | 7 | from pyupgrade._plugins.typing_pep604 import _fix_union 8 | 9 | 10 | @pytest.mark.parametrize( 11 | ('s', 'arg_count', 'expected'), 12 | ( 13 | ('Union[a, b]', 2, 'a | b'), 14 | ('Union[(a, b)]', 2, 'a | b'), 15 | ('Union[(a,)]', 1, 'a'), 16 | ('Union[(((a, b)))]', 2, 'a | b'), 17 | pytest.param('Union[((a), b)]', 2, '(a) | b', id='wat'), 18 | ('Union[(((a,), b))]', 2, '(a,) | b'), 19 | ('Union[((a,), (a, b))]', 2, '(a,) | (a, b)'), 20 | ('Union[((a))]', 1, 'a'), 21 | ('Union[a()]', 1, 'a()'), 22 | ('Union[a(b, c)]', 1, 'a(b, c)'), 23 | ('Union[(a())]', 1, 'a()'), 24 | ('Union[(())]', 1, '()'), 25 | ), 26 | ) 27 | def test_fix_union_edge_cases(s, arg_count, expected): 28 | tokens = src_to_tokens(s) 29 | _fix_union(0, tokens, arg_count=arg_count) 30 | assert tokens_to_src(tokens) == expected 31 | -------------------------------------------------------------------------------- /tests/features/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asottile/pyupgrade/1a0b8a1996416e51e374431887b12aae3f8d3e80/tests/features/__init__.py -------------------------------------------------------------------------------- /tests/features/binary_literals_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from pyupgrade._main import _fix_tokens 6 | 7 | 8 | @pytest.mark.parametrize( 9 | 's', 10 | ( 11 | '"☃".encode("UTF-8")', 12 | '"\\u2603".encode("UTF-8")', 13 | '"\\U0001f643".encode("UTF-8")', 14 | '"\\N{SNOWMAN}".encode("UTF-8")', 15 | '"\\xa0".encode("UTF-8")', 16 | # not byte literal compatible 17 | '"y".encode("utf16")', 18 | # can't rewrite f-strings 19 | 'f"{x}".encode()', 20 | # not a `.encode()` call 21 | '"foo".encode', '("foo".encode)', 22 | # encode, but not a literal 23 | 'x.encode()', 24 | # the codec / string is an f-string 25 | 'str.encode(f"{c}")', '"foo".encode(f"{c}")', 26 | pytest.param('wat.encode(b"unrelated")', id='unrelated .encode(...)'), 27 | ), 28 | ) 29 | def test_binary_literals_noop(s): 30 | assert _fix_tokens(s) == s 31 | 32 | 33 | @pytest.mark.parametrize( 34 | ('s', 'expected'), 35 | ( 36 | ('"foo".encode()', 'b"foo"'), 37 | ('"foo".encode("ascii")', 'b"foo"'), 38 | ('"foo".encode("utf-8")', 'b"foo"'), 39 | ('"\\xa0".encode("latin1")', 'b"\\xa0"'), 40 | (r'"\\u wot".encode()', r'b"\\u wot"'), 41 | (r'"\\x files".encode()', r'b"\\x files"'), 42 | ( 43 | 'f(\n' 44 | ' "foo"\n' 45 | ' "bar".encode()\n' 46 | ')\n', 47 | 48 | 'f(\n' 49 | ' b"foo"\n' 50 | ' b"bar"\n' 51 | ')\n', 52 | ), 53 | ), 54 | ) 55 | def test_binary_literals(s, expected): 56 | assert _fix_tokens(s) == expected 57 | -------------------------------------------------------------------------------- /tests/features/capture_output_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from pyupgrade._data import Settings 6 | from pyupgrade._main import _fix_plugins 7 | 8 | 9 | @pytest.mark.parametrize( 10 | ('s', 'version'), 11 | ( 12 | pytest.param( 13 | 'import subprocess\n' 14 | 'subprocess.run(["foo"], stdout=subprocess.PIPE, ' 15 | 'stderr=subprocess.PIPE)\n', 16 | (3,), 17 | id='not Python3.7+', 18 | ), 19 | pytest.param( 20 | 'from foo import run\n' 21 | 'import subprocess\n' 22 | 'run(["foo"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n', 23 | (3, 7), 24 | id='run imported, but not from subprocess', 25 | ), 26 | pytest.param( 27 | 'from foo import PIPE\n' 28 | 'from subprocess import run\n' 29 | 'subprocess.run(["foo"], stdout=PIPE, stderr=PIPE)\n', 30 | (3, 7), 31 | id='PIPE imported, but not from subprocess', 32 | ), 33 | pytest.param( 34 | 'from subprocess import run\n' 35 | 'run(["foo"], stdout=None, stderr=PIPE)\n', 36 | (3, 7), 37 | id='stdout not subprocess.PIPE', 38 | ), 39 | ), 40 | ) 41 | def test_fix_capture_output_noop(s, version): 42 | assert _fix_plugins(s, settings=Settings(min_version=version)) == s 43 | 44 | 45 | @pytest.mark.parametrize( 46 | ('s', 'expected'), 47 | ( 48 | pytest.param( 49 | 'import subprocess\n' 50 | 'subprocess.run(["foo"], stdout=subprocess.PIPE, ' 51 | 'stderr=subprocess.PIPE)\n', 52 | 'import subprocess\n' 53 | 'subprocess.run(["foo"], capture_output=True)\n', 54 | id='subprocess.run and subprocess.PIPE attributes', 55 | ), 56 | pytest.param( 57 | 'from subprocess import run, PIPE\n' 58 | 'run(["foo"], stdout=PIPE, stderr=PIPE)\n', 59 | 'from subprocess import run, PIPE\n' 60 | 'run(["foo"], capture_output=True)\n', 61 | id='run and PIPE imported from subprocess', 62 | ), 63 | pytest.param( 64 | 'from subprocess import run, PIPE\n' 65 | 'run(["foo"], shell=True, stdout=PIPE, stderr=PIPE)\n', 66 | 'from subprocess import run, PIPE\n' 67 | 'run(["foo"], shell=True, capture_output=True)\n', 68 | id='other argument used too', 69 | ), 70 | pytest.param( 71 | 'import subprocess\n' 72 | 'subprocess.run(["foo"], stderr=subprocess.PIPE, ' 73 | 'stdout=subprocess.PIPE)\n', 74 | 'import subprocess\n' 75 | 'subprocess.run(["foo"], capture_output=True)\n', 76 | id='stderr used before stdout', 77 | ), 78 | pytest.param( 79 | 'import subprocess\n' 80 | 'subprocess.run(stderr=subprocess.PIPE, args=["foo"], ' 81 | 'stdout=subprocess.PIPE)\n', 82 | 'import subprocess\n' 83 | 'subprocess.run(args=["foo"], capture_output=True)\n', 84 | id='stdout is first argument', 85 | ), 86 | pytest.param( 87 | 'import subprocess\n' 88 | 'subprocess.run(\n' 89 | ' stderr=subprocess.PIPE, \n' 90 | ' args=["foo"], \n' 91 | ' stdout=subprocess.PIPE,\n' 92 | ')\n', 93 | 'import subprocess\n' 94 | 'subprocess.run(\n' 95 | ' args=["foo"], \n' 96 | ' capture_output=True,\n' 97 | ')\n', 98 | id='stdout is first argument, multiline', 99 | ), 100 | pytest.param( 101 | 'subprocess.run(\n' 102 | ' "foo",\n' 103 | ' stdout=subprocess.PIPE,\n' 104 | ' stderr=subprocess.PIPE,\n' 105 | ' universal_newlines=True,\n' 106 | ')', 107 | 'subprocess.run(\n' 108 | ' "foo",\n' 109 | ' capture_output=True,\n' 110 | ' text=True,\n' 111 | ')', 112 | id='both universal_newlines and capture_output rewrite', 113 | ), 114 | pytest.param( 115 | 'subprocess.run(\n' 116 | ' f"{x}(",\n' 117 | ' stdout=subprocess.PIPE,\n' 118 | ' stderr=subprocess.PIPE,\n' 119 | ')', 120 | 121 | 'subprocess.run(\n' 122 | ' f"{x}(",\n' 123 | ' capture_output=True,\n' 124 | ')', 125 | 126 | id='3.12: fstring with open brace', 127 | ), 128 | pytest.param( 129 | 'subprocess.run(\n' 130 | ' f"{x})",\n' 131 | ' stdout=subprocess.PIPE,\n' 132 | ' stderr=subprocess.PIPE,\n' 133 | ')', 134 | 135 | 'subprocess.run(\n' 136 | ' f"{x})",\n' 137 | ' capture_output=True,\n' 138 | ')', 139 | 140 | id='3.12: fstring with close brace', 141 | ), 142 | ), 143 | ) 144 | def test_fix_capture_output(s, expected): 145 | ret = _fix_plugins(s, settings=Settings(min_version=(3, 7))) 146 | assert ret == expected 147 | -------------------------------------------------------------------------------- /tests/features/collections_abc_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from pyupgrade._data import Settings 6 | from pyupgrade._main import _fix_plugins 7 | 8 | 9 | def test_collections_abc_noop(): 10 | src = 'if isinstance(x, collections.defaultdict): pass\n' 11 | assert _fix_plugins(src, settings=Settings()) == src 12 | 13 | 14 | @pytest.mark.parametrize( 15 | ('src', 'expected'), 16 | ( 17 | pytest.param( 18 | 'if isinstance(x, collections.Sized):\n' 19 | ' print(len(x))\n', 20 | 'if isinstance(x, collections.abc.Sized):\n' 21 | ' print(len(x))\n', 22 | id='Attribute reference for Sized class', 23 | ), 24 | ), 25 | ) 26 | def test_collections_abc_rewrite(src, expected): 27 | assert _fix_plugins(src, settings=Settings()) == expected 28 | -------------------------------------------------------------------------------- /tests/features/constant_fold_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from pyupgrade._data import Settings 6 | from pyupgrade._main import _fix_plugins 7 | 8 | 9 | @pytest.mark.parametrize( 10 | 's', 11 | ( 12 | pytest.param( 13 | 'isinstance(x, str)', 14 | id='isinstance nothing duplicated', 15 | ), 16 | pytest.param( 17 | 'issubclass(x, str)', 18 | id='issubclass nothing duplicated', 19 | ), 20 | pytest.param( 21 | 'try: ...\n' 22 | 'except Exception: ...\n', 23 | id='try-except nothing duplicated', 24 | ), 25 | pytest.param( 26 | 'isinstance(x, (str, (str,)))', 27 | id='only consider flat tuples', 28 | ), 29 | pytest.param( 30 | 'isinstance(x, (f(), a().g))', 31 | id='only consider names and dotted names', 32 | ), 33 | ), 34 | ) 35 | def test_constant_fold_noop(s): 36 | assert _fix_plugins(s, settings=Settings()) == s 37 | 38 | 39 | @pytest.mark.parametrize( 40 | ('s', 'expected'), 41 | ( 42 | pytest.param( 43 | 'isinstance(x, (str, str, int))', 44 | 45 | 'isinstance(x, (str, int))', 46 | 47 | id='isinstance', 48 | ), 49 | pytest.param( 50 | 'issubclass(x, (str, str, int))', 51 | 52 | 'issubclass(x, (str, int))', 53 | 54 | id='issubclass', 55 | ), 56 | pytest.param( 57 | 'try: ...\n' 58 | 'except (Exception, Exception, TypeError): ...\n', 59 | 60 | 'try: ...\n' 61 | 'except (Exception, TypeError): ...\n', 62 | 63 | id='except', 64 | ), 65 | 66 | pytest.param( 67 | 'isinstance(x, (str, str))', 68 | 69 | 'isinstance(x, str)', 70 | 71 | id='folds to 1', 72 | ), 73 | 74 | pytest.param( 75 | 'isinstance(x, (a.b, a.b, a.c))', 76 | 'isinstance(x, (a.b, a.c))', 77 | id='folds dotted names', 78 | ), 79 | pytest.param( 80 | 'try: ...\n' 81 | 'except(a, a): ...\n', 82 | 83 | 'try: ...\n' 84 | 'except a: ...\n', 85 | 86 | id='deduplication to 1 does not cause syntax error with except', 87 | ), 88 | ), 89 | ) 90 | def test_constant_fold(s, expected): 91 | assert _fix_plugins(s, settings=Settings()) == expected 92 | -------------------------------------------------------------------------------- /tests/features/datetime_utc_alias_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from pyupgrade._data import Settings 6 | from pyupgrade._main import _fix_plugins 7 | 8 | 9 | @pytest.mark.parametrize( 10 | ('s',), 11 | ( 12 | pytest.param( 13 | 'import datetime\n' 14 | 'print(datetime.timezone(-1))', 15 | 16 | id='not rewriting timezone object to alias', 17 | ), 18 | ), 19 | ) 20 | def test_fix_datetime_utc_alias_noop(s): 21 | assert _fix_plugins(s, settings=Settings(min_version=(3,))) == s 22 | assert _fix_plugins(s, settings=Settings(min_version=(3, 11))) == s 23 | 24 | 25 | @pytest.mark.parametrize( 26 | ('s', 'expected'), 27 | ( 28 | pytest.param( 29 | 'import datetime\n' 30 | 'print(datetime.timezone.utc)', 31 | 32 | 'import datetime\n' 33 | 'print(datetime.UTC)', 34 | 35 | id='rewriting to alias', 36 | ), 37 | ), 38 | ) 39 | def test_fix_datetime_utc_alias(s, expected): 40 | assert _fix_plugins(s, settings=Settings(min_version=(3,))) == s 41 | assert _fix_plugins(s, settings=Settings(min_version=(3, 11))) == expected 42 | -------------------------------------------------------------------------------- /tests/features/default_encoding_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from pyupgrade._data import Settings 6 | from pyupgrade._main import _fix_plugins 7 | 8 | 9 | @pytest.mark.parametrize( 10 | ('s', 'expected'), 11 | ( 12 | ('"asd".encode("utf-8")', '"asd".encode()'), 13 | ('f"asd".encode("utf-8")', 'f"asd".encode()'), 14 | ('f"{3}asd".encode("utf-8")', 'f"{3}asd".encode()'), 15 | ('fr"asd".encode("utf-8")', 'fr"asd".encode()'), 16 | ('r"asd".encode("utf-8")', 'r"asd".encode()'), 17 | ('"asd".encode("utf8")', '"asd".encode()'), 18 | ('"asd".encode("UTF-8")', '"asd".encode()'), 19 | pytest.param( 20 | '"asd".encode(("UTF-8"))', 21 | '"asd".encode()', 22 | id='parenthesized encoding', 23 | ), 24 | ( 25 | 'sys.stdout.buffer.write(\n "a"\n "b".encode("utf-8")\n)', 26 | 'sys.stdout.buffer.write(\n "a"\n "b".encode()\n)', 27 | ), 28 | ( 29 | 'x = (\n' 30 | ' "y\\u2603"\n' 31 | ').encode("utf-8")\n', 32 | 'x = (\n' 33 | ' "y\\u2603"\n' 34 | ').encode()\n', 35 | ), 36 | pytest.param( 37 | 'f"{x}(".encode("utf-8")', 38 | 'f"{x}(".encode()', 39 | id='3.12+ handle open brace in fstring', 40 | ), 41 | pytest.param( 42 | 'f"{foo(bar)}(".encode("utf-8")', 43 | 'f"{foo(bar)}(".encode()', 44 | id='f-string with function call', 45 | ), 46 | ), 47 | ) 48 | def test_fix_encode(s, expected): 49 | ret = _fix_plugins(s, settings=Settings()) 50 | assert ret == expected 51 | 52 | 53 | @pytest.mark.parametrize( 54 | 's', 55 | ( 56 | # non-utf-8 codecs should not be changed 57 | '"asd".encode("unknown-codec")', 58 | '"asd".encode("ascii")', 59 | 60 | # only autofix string literals to avoid false positives 61 | 'x="asd"\nx.encode("utf-8")', 62 | 63 | # the current version is too timid to handle these 64 | '"asd".encode("utf-8", "strict")', 65 | '"asd".encode(encoding="utf-8")', 66 | ), 67 | ) 68 | def test_fix_encode_noop(s): 69 | assert _fix_plugins(s, settings=Settings()) == s 70 | -------------------------------------------------------------------------------- /tests/features/dict_literals_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from pyupgrade._data import Settings 6 | from pyupgrade._main import _fix_plugins 7 | 8 | 9 | @pytest.mark.parametrize( 10 | 's', 11 | ( 12 | # Don't touch irrelevant code 13 | 'x = 5', 14 | 'dict()', 15 | # Don't touch syntax errors 16 | '(', 17 | # Don't touch strange looking calls 18 | 'dict ((a, b) for a, b in y)', 19 | # Don't rewrite kwargd dicts 20 | 'dict(((a, b) for a, b in y), x=1)', 21 | 'dict(((a, b) for a, b in y), **kwargs)', 22 | pytest.param( 23 | 'f"{dict((a, b) for a, b in y)}"', 24 | id='directly inside f-string placeholder', 25 | ), 26 | ), 27 | ) 28 | def test_fix_dict_noop(s): 29 | assert _fix_plugins(s, settings=Settings()) == s 30 | 31 | 32 | @pytest.mark.parametrize( 33 | ('s', 'expected'), 34 | ( 35 | # dict of generator expression 36 | ('dict((a, b) for a, b in y)', '{a: b for a, b in y}'), 37 | ('dict((a, b,) for a, b in y)', '{a: b for a, b in y}'), 38 | ('dict((a, b, ) for a, b in y)', '{a: b for a, b in y}'), 39 | ('dict([a, b] for a, b in y)', '{a: b for a, b in y}'), 40 | # Parenthesized target 41 | ('dict(((a, b)) for a, b in y)', '{a: b for a, b in y}'), 42 | # dict of list comprehension 43 | ('dict([(a, b) for a, b in y])', '{a: b for a, b in y}'), 44 | # ast doesn't tell us about the tuple in the list 45 | ('dict([(a, b), c] for a, b, c in y)', '{(a, b): c for a, b, c in y}'), 46 | # ast doesn't tell us about parenthesized keys 47 | ('dict(((a), b) for a, b in y)', '{(a): b for a, b in y}'), 48 | # Nested dictcomps 49 | ( 50 | 'dict((k, dict((k2, v2) for k2, v2 in y2)) for k, y2 in y)', 51 | '{k: {k2: v2 for k2, v2 in y2} for k, y2 in y}', 52 | ), 53 | # This doesn't get fixed by autopep8 and can cause a syntax error 54 | ('dict((a, b)for a, b in y)', '{a: b for a, b in y}'), 55 | # Need to remove trailing commas on the element 56 | ( 57 | 'dict(\n' 58 | ' (\n' 59 | ' a,\n' 60 | ' b,\n' 61 | ' )\n' 62 | ' for a, b in y\n' 63 | ')', 64 | # Ideally, this'll go through some other formatting tool before 65 | # being committed. Shrugs! 66 | '{\n' 67 | ' a:\n' 68 | ' b\n' 69 | ' for a, b in y\n' 70 | '}', 71 | ), 72 | # Don't gobble the last paren in a dictcomp 73 | ( 74 | 'x(\n' 75 | ' dict(\n' 76 | ' (a, b) for a, b in y\n' 77 | ' )\n' 78 | ')', 79 | 'x(\n' 80 | ' {\n' 81 | ' a: b for a, b in y\n' 82 | ' }\n' 83 | ')', 84 | ), 85 | ), 86 | ) 87 | def test_dictcomps(s, expected): 88 | ret = _fix_plugins(s, settings=Settings()) 89 | assert ret == expected 90 | -------------------------------------------------------------------------------- /tests/features/encoding_cookie_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from pyupgrade._main import _fix_tokens 6 | 7 | 8 | @pytest.mark.parametrize( 9 | 's', 10 | ( 11 | pytest.param( 12 | '# line 1\n# line 2\n# coding: utf-8\n', 13 | id='only on first two lines', 14 | ), 15 | ), 16 | ) 17 | def test_noop(s): 18 | assert _fix_tokens(s) == s 19 | 20 | 21 | @pytest.mark.parametrize( 22 | ('s', 'expected'), 23 | ( 24 | ( 25 | '# coding: utf-8', 26 | '', 27 | ), 28 | ( 29 | '# coding: us-ascii\nx = 1\n', 30 | 'x = 1\n', 31 | ), 32 | ( 33 | '#!/usr/bin/env python\n' 34 | '# coding: utf-8\n' 35 | 'x = 1\n', 36 | 37 | '#!/usr/bin/env python\n' 38 | 'x = 1\n', 39 | ), 40 | ), 41 | ) 42 | def test_rewrite(s, expected): 43 | assert _fix_tokens(s) == expected 44 | -------------------------------------------------------------------------------- /tests/features/escape_sequences_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from pyupgrade._main import _fix_tokens 6 | 7 | 8 | @pytest.mark.parametrize( 9 | 's', 10 | ( 11 | '""', 12 | r'r"\d"', r"r'\d'", r'r"""\d"""', r"r'''\d'''", 13 | r'rb"\d"', 14 | # make sure we don't replace an already valid string 15 | r'"\\d"', 16 | # this is already a proper unicode escape 17 | r'"\u2603"', 18 | # don't touch already valid escapes 19 | r'"\r\n"', 20 | # python3.3+ named unicode escapes 21 | r'"\N{SNOWMAN}"', 22 | # don't touch escaped newlines 23 | '"""\\\n"""', '"""\\\r\n"""', '"""\\\r"""', 24 | ), 25 | ) 26 | def test_fix_escape_sequences_noop(s): 27 | assert _fix_tokens(s) == s 28 | 29 | 30 | @pytest.mark.parametrize( 31 | ('s', 'expected'), 32 | ( 33 | # no valid escape sequences, make a raw literal 34 | (r'"\d"', r'r"\d"'), 35 | # when there are valid escape sequences, need to use backslashes 36 | (r'"\n\d"', r'"\n\\d"'), 37 | # this gets un-u'd and raw'd 38 | (r'u"\d"', r'r"\d"'), 39 | # `rb` is not a valid string prefix in python2.x 40 | (r'b"\d"', r'br"\d"'), 41 | # 8 and 9 aren't valid octal digits 42 | (r'"\8"', r'r"\8"'), (r'"\9"', r'r"\9"'), 43 | # explicit byte strings should not honor string-specific escapes 44 | ('b"\\u2603"', 'br"\\u2603"'), 45 | # do not make a raw string for escaped newlines 46 | ('"""\\\n\\q"""', '"""\\\n\\\\q"""'), 47 | ('"""\\\r\n\\q"""', '"""\\\r\n\\\\q"""'), 48 | ('"""\\\r\\q"""', '"""\\\r\\\\q"""'), 49 | # python2.x allows \N, in python3.3+ this is a syntax error 50 | (r'"\N"', r'r"\N"'), (r'"\N\n"', r'"\\N\n"'), 51 | (r'"\N{SNOWMAN}\q"', r'"\N{SNOWMAN}\\q"'), 52 | (r'b"\N{SNOWMAN}"', r'br"\N{SNOWMAN}"'), 53 | ), 54 | ) 55 | def test_fix_escape_sequences(s, expected): 56 | assert _fix_tokens(s) == expected 57 | -------------------------------------------------------------------------------- /tests/features/extra_parens_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from pyupgrade._main import _fix_tokens 6 | 7 | 8 | @pytest.mark.parametrize( 9 | 's', 10 | ( 11 | 'print("hello world")', 12 | 'print((1, 2, 3))', 13 | 'print(())', 14 | 'print((\n))', 15 | # don't touch parenthesized generators 16 | 'sum((block.code for block in blocks), [])', 17 | # don't touch coroutine yields 18 | 'def f():\n' 19 | ' x = int((yield 1))\n', 20 | ), 21 | ) 22 | def test_fix_extra_parens_noop(s): 23 | assert _fix_tokens(s) == s 24 | 25 | 26 | @pytest.mark.parametrize( 27 | ('s', 'expected'), 28 | ( 29 | ('print(("hello world"))', 'print("hello world")'), 30 | ('print(("foo{}".format(1)))', 'print("foo{}".format(1))'), 31 | ('print((((1))))', 'print(1)'), 32 | ( 33 | 'print(\n' 34 | ' ("foo{}".format(1))\n' 35 | ')', 36 | 37 | 'print(\n' 38 | ' "foo{}".format(1)\n' 39 | ')', 40 | ), 41 | ( 42 | 'print(\n' 43 | ' (\n' 44 | ' "foo"\n' 45 | ' )\n' 46 | ')\n', 47 | 48 | 'print(\n' 49 | ' "foo"\n' 50 | ')\n', 51 | ), 52 | pytest.param( 53 | 'def f():\n' 54 | ' x = int(((yield 1)))\n', 55 | 56 | 'def f():\n' 57 | ' x = int((yield 1))\n', 58 | 59 | id='extra parens on coroutines are instead reduced to 2', 60 | ), 61 | pytest.param( 62 | 'f((f"{x})"))', 63 | 'f(f"{x})")', 64 | id='3.12: handle close brace in fstring body', 65 | ), 66 | pytest.param( 67 | 'f((f"{x}("))', 68 | 'f(f"{x}(")', 69 | id='3.12: handle open brace in fstring body', 70 | ), 71 | ), 72 | ) 73 | def test_fix_extra_parens(s, expected): 74 | assert _fix_tokens(s) == expected 75 | -------------------------------------------------------------------------------- /tests/features/format_literals_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from pyupgrade._main import _fix_tokens 6 | 7 | 8 | @pytest.mark.parametrize( 9 | 's', 10 | ( 11 | # Don't touch syntax errors 12 | '"{0}"format(1)', 13 | pytest.param("'{}'.format(1)", id='already upgraded'), 14 | # Don't touch invalid format strings 15 | "'{'.format(1)", "'}'.format(1)", 16 | # Don't touch non-format strings 17 | "x = ('{0} {1}',)\n", 18 | # Don't touch non-incrementing integers 19 | "'{0} {0}'.format(1)", 20 | # Formats can be embedded in formats, leave these alone? 21 | "'{0:<{1}}'.format(1, 4)", 22 | # don't attempt to fix this, garbage in garbage out 23 | "'{' '0}'.format(1)", 24 | # comment looks like placeholder but is not! 25 | '("{0}" # {1}\n"{2}").format(1, 2, 3)', 26 | # don't touch f-strings (these are wrong but don't make it worse) 27 | 'f"{0}".format(a)', 28 | # shouldn't touch the format spec 29 | r'"{}\N{SNOWMAN}".format("")', 30 | ), 31 | ) 32 | def test_format_literals_noop(s): 33 | assert _fix_tokens(s) == s 34 | 35 | 36 | @pytest.mark.parametrize( 37 | ('s', 'expected'), 38 | ( 39 | # Simplest case 40 | ("'{0}'.format(1)", "'{}'.format(1)"), 41 | ("'{0:x}'.format(30)", "'{:x}'.format(30)"), 42 | ("x = '{0}'.format(1)", "x = '{}'.format(1)"), 43 | # Multiline strings 44 | ("'''{0}\n{1}\n'''.format(1, 2)", "'''{}\n{}\n'''.format(1, 2)"), 45 | # Multiple implicitly-joined strings 46 | ("'{0}' '{1}'.format(1, 2)", "'{}' '{}'.format(1, 2)"), 47 | # Multiple implicitly-joined strings over lines 48 | ( 49 | 'print(\n' 50 | " 'foo{0}'\n" 51 | " 'bar{1}'.format(1, 2)\n" 52 | ')', 53 | 'print(\n' 54 | " 'foo{}'\n" 55 | " 'bar{}'.format(1, 2)\n" 56 | ')', 57 | ), 58 | # Multiple implicitly-joind strings over lines with comments 59 | ( 60 | 'print(\n' 61 | " 'foo{0}' # ohai\n" 62 | " 'bar{1}'.format(1, 2)\n" 63 | ')', 64 | 'print(\n' 65 | " 'foo{}' # ohai\n" 66 | " 'bar{}'.format(1, 2)\n" 67 | ')', 68 | ), 69 | # joined by backslash 70 | ( 71 | 'x = "foo {0}" \\\n' 72 | ' "bar {1}".format(1, 2)', 73 | 'x = "foo {}" \\\n' 74 | ' "bar {}".format(1, 2)', 75 | ), 76 | # parenthesized string literals 77 | ('("{0}").format(1)', '("{}").format(1)'), 78 | pytest.param( 79 | r'"\N{snowman} {0}".format(1)', 80 | r'"\N{snowman} {}".format(1)', 81 | id='named escape sequence', 82 | ), 83 | ), 84 | ) 85 | def test_format_literals(s, expected): 86 | assert _fix_tokens(s) == expected 87 | -------------------------------------------------------------------------------- /tests/features/format_locals_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from pyupgrade._data import Settings 6 | from pyupgrade._main import _fix_plugins 7 | 8 | 9 | @pytest.mark.parametrize( 10 | ('s', 'version'), 11 | ( 12 | pytest.param( 13 | '"{x}".format(**locals())', 14 | (3,), 15 | id='not 3.6+', 16 | ), 17 | pytest.param( 18 | '"{x} {y}".format(x, **locals())', 19 | (3, 6), 20 | id='mixed locals() and params', 21 | ), 22 | ), 23 | ) 24 | def test_fix_format_locals_noop(s, version): 25 | assert _fix_plugins(s, settings=Settings(min_version=version)) == s 26 | 27 | 28 | @pytest.mark.parametrize( 29 | ('s', 'expected'), 30 | ( 31 | pytest.param( 32 | '"{x}".format(**locals())', 33 | 'f"{x}"', 34 | id='normal case', 35 | ), 36 | pytest.param( 37 | '"{x}" "{y}".format(**locals())', 38 | 'f"{x}" f"{y}"', 39 | id='joined strings', 40 | ), 41 | pytest.param( 42 | '(\n' 43 | ' "{x}"\n' 44 | ' "{y}"\n' 45 | ').format(**locals())\n', 46 | '(\n' 47 | ' f"{x}"\n' 48 | ' f"{y}"\n' 49 | ')\n', 50 | id='joined strings with parens', 51 | ), 52 | ), 53 | ) 54 | def test_fix_format_locals(s, expected): 55 | assert _fix_plugins(s, settings=Settings(min_version=(3, 6))) == expected 56 | -------------------------------------------------------------------------------- /tests/features/fstrings_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from pyupgrade._data import Settings 6 | from pyupgrade._main import _fix_plugins 7 | 8 | 9 | @pytest.mark.parametrize( 10 | 's', 11 | ( 12 | # syntax error 13 | '(', 14 | # invalid format strings 15 | "'{'.format(a)", "'}'.format(a)", 16 | # weird syntax 17 | '"{}" . format(x)', 18 | # spans multiple lines 19 | '"{}".format(\n a,\n)', 20 | # starargs 21 | '"{} {}".format(*a)', '"{foo} {bar}".format(**b)"', 22 | # likely makes the format longer 23 | '"{0} {0}".format(arg)', '"{x} {x}".format(arg)', 24 | '"{x.y} {x.z}".format(arg)', 25 | # bytestrings don't participate in `.format()` or `f''` 26 | # but are legal in python 2 27 | 'b"{} {}".format(a, b)', 28 | # for now, too difficult to rewrite correctly 29 | '"{:{}}".format(x, y)', 30 | '"{a[b]}".format(a=a)', 31 | '"{a.a[b]}".format(a=a)', 32 | # not enough placeholders / placeholders missing 33 | '"{}{}".format(a)', '"{a}{b}".format(a=a)', 34 | # backslashes and quotes cannot nest 35 | r'''"{}".format(a['\\'])''', 36 | '"{}".format(a["b"])', 37 | "'{}'.format(a['b'])", 38 | # await only becomes keyword in Python 3.7+ 39 | "async def c(): return '{}'.format(await 3)", 40 | "async def c(): return '{}'.format(1 + await 3)", 41 | ), 42 | ) 43 | def test_fix_fstrings_noop(s): 44 | assert _fix_plugins(s, settings=Settings(min_version=(3, 6))) == s 45 | 46 | 47 | @pytest.mark.parametrize( 48 | ('s', 'expected'), 49 | ( 50 | ('"{} {}".format(a, b)', 'f"{a} {b}"'), 51 | ('"{1} {0}".format(a, b)', 'f"{b} {a}"'), 52 | ('"{x.y}".format(x=z)', 'f"{z.y}"'), 53 | ('"{.x} {.y}".format(a, b)', 'f"{a.x} {b.y}"'), 54 | ('"{} {}".format(a.b, c.d)', 'f"{a.b} {c.d}"'), 55 | ('"{}".format(a())', 'f"{a()}"'), 56 | ('"{}".format(a.b())', 'f"{a.b()}"'), 57 | ('"{}".format(a.b().c())', 'f"{a.b().c()}"'), 58 | ('"hello {}!".format(name)', 'f"hello {name}!"'), 59 | ('"{}{{}}{}".format(escaped, y)', 'f"{escaped}{{}}{y}"'), 60 | ('"{}{b}{}".format(a, c, b=b)', 'f"{a}{b}{c}"'), 61 | ('"{}".format(0x0)', 'f"{0x0}"'), 62 | pytest.param( 63 | r'"\N{snowman} {}".format(a)', 64 | r'f"\N{snowman} {a}"', 65 | id='named escape sequences', 66 | ), 67 | pytest.param( 68 | 'u"foo{}".format(1)', 69 | 'f"foo{1}"', 70 | id='u-prefixed format', 71 | ), 72 | ), 73 | ) 74 | def test_fix_fstrings(s, expected): 75 | assert _fix_plugins(s, settings=Settings(min_version=(3, 6))) == expected 76 | 77 | 78 | def test_fix_fstrings_await_py37(): 79 | s = "async def c(): return '{}'.format(await 1+foo())" 80 | expected = "async def c(): return f'{await 1+foo()}'" 81 | assert _fix_plugins(s, settings=Settings(min_version=(3, 7))) == expected 82 | -------------------------------------------------------------------------------- /tests/features/identity_equality_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from pyupgrade._data import Settings 6 | from pyupgrade._main import _fix_plugins 7 | 8 | 9 | @pytest.mark.parametrize( 10 | 's', 11 | ( 12 | 'x is True', 13 | 'x is False', 14 | 'x is None', 15 | 'x is (not 5)', 16 | 'x is 5 + 5', 17 | # pyupgrade is timid about containers since the original can be 18 | # always-False, but the rewritten code could be `True`. 19 | 'x is ()', 20 | 'x is []', 21 | 'x is {}', 22 | 'x is {1}', 23 | ), 24 | ) 25 | def test_fix_is_compare_to_literal_noop(s): 26 | assert _fix_plugins(s, settings=Settings()) == s 27 | 28 | 29 | @pytest.mark.parametrize( 30 | ('s', 'expected'), 31 | ( 32 | pytest.param('x is 5', 'x == 5', id='`is`'), 33 | pytest.param('x is not 5', 'x != 5', id='`is not`'), 34 | pytest.param('x is ""', 'x == ""', id='string'), 35 | pytest.param('x is u""', 'x == u""', id='unicode string'), 36 | pytest.param('x is b""', 'x == b""', id='bytes'), 37 | pytest.param('x is 1.5', 'x == 1.5', id='float'), 38 | pytest.param('x == 5 is 5', 'x == 5 == 5', id='compound compare'), 39 | pytest.param( 40 | 'if (\n' 41 | ' x is\n' 42 | ' 5\n' 43 | '): pass\n', 44 | 45 | 'if (\n' 46 | ' x ==\n' 47 | ' 5\n' 48 | '): pass\n', 49 | 50 | id='multi-line `is`', 51 | ), 52 | pytest.param( 53 | 'if (\n' 54 | ' x is\n' 55 | ' not 5\n' 56 | '): pass\n', 57 | 58 | 'if (\n' 59 | ' x != 5\n' 60 | '): pass\n', 61 | 62 | id='multi-line `is not`', 63 | ), 64 | ), 65 | ) 66 | def test_fix_is_compare_to_literal(s, expected): 67 | ret = _fix_plugins(s, settings=Settings()) 68 | assert ret == expected 69 | -------------------------------------------------------------------------------- /tests/features/import_removals_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from pyupgrade._data import Settings 6 | from pyupgrade._main import _fix_plugins 7 | 8 | 9 | @pytest.mark.parametrize( 10 | ('s', 'min_version'), 11 | ( 12 | ('', (3,)), 13 | ('from foo import bar', (3,)), 14 | ('from __future__ import unknown', (3,)), 15 | ('from __future__ import annotations', (3,)), 16 | ('from six import *', (3,)), 17 | ('from six.moves import map as notmap', (3,)), 18 | ('from unrelated import queue as map', (3,)), 19 | pytest.param( 20 | 'if True:\n' 21 | ' from six.moves import map\n', 22 | (3,), 23 | id='import removal not at module scope', 24 | ), 25 | ), 26 | ) 27 | def test_import_removals_noop(s, min_version): 28 | assert _fix_plugins(s, settings=Settings(min_version=min_version)) == s 29 | 30 | 31 | @pytest.mark.parametrize( 32 | ('s', 'min_version', 'expected'), 33 | ( 34 | ('from __future__ import generators\n', (3,), ''), 35 | ('from __future__ import generators', (3,), ''), 36 | ('from __future__ import division\n', (3,), ''), 37 | ('from __future__ import division\n', (3, 6), ''), 38 | ('from __future__ import (generators,)', (3,), ''), 39 | ('from __future__ import print_function', (3, 8), ''), 40 | ('from builtins import map', (3,), ''), 41 | ('from builtins import *', (3,), ''), 42 | ('from six.moves import map', (3,), ''), 43 | ('from six.moves.builtins import map', (3,), ''), 44 | pytest.param( 45 | 'from __future__ import absolute_import, annotations\n', 46 | (3,), 47 | 'from __future__ import annotations\n', 48 | id='remove at beginning single line', 49 | ), 50 | pytest.param( 51 | 'from __future__ import (\n' 52 | ' absolute_import,\n' 53 | ' annotations,\n' 54 | ')', 55 | (3,), 56 | 'from __future__ import (\n' 57 | ' annotations,\n' 58 | ')', 59 | id='remove at beginning paren continuation', 60 | ), 61 | pytest.param( 62 | 'from __future__ import \\\n' 63 | ' absolute_import, \\\n' 64 | ' annotations\n', 65 | (3,), 66 | 'from __future__ import \\\n' 67 | ' annotations\n', 68 | id='remove at beginning backslash continuation', 69 | ), 70 | pytest.param( 71 | 'from __future__ import annotations, absolute_import\n', 72 | (3,), 73 | 'from __future__ import annotations\n', 74 | id='remove at end single line', 75 | ), 76 | pytest.param( 77 | 'from __future__ import (\n' 78 | ' annotations,\n' 79 | ' absolute_import,\n' 80 | ')', 81 | (3,), 82 | 'from __future__ import (\n' 83 | ' annotations,\n' 84 | ')', 85 | id='remove at end paren continuation', 86 | ), 87 | pytest.param( 88 | 'from __future__ import \\\n' 89 | ' annotations, \\\n' 90 | ' absolute_import\n', 91 | (3,), 92 | 'from __future__ import \\\n' 93 | ' annotations\n', 94 | id='remove at end backslash continuation', 95 | ), 96 | pytest.param( 97 | 'from __future__ import (\n' 98 | ' absolute_import,\n' 99 | ' annotations,\n' 100 | ' division,\n' 101 | ')', 102 | (3,), 103 | 'from __future__ import (\n' 104 | ' annotations,\n' 105 | ')', 106 | id='remove multiple', 107 | ), 108 | pytest.param( 109 | 'from __future__ import with_statement\n' 110 | '\n' 111 | 'import os.path\n', 112 | (3,), 113 | 'import os.path\n', 114 | id='remove top-file whitespace', 115 | ), 116 | pytest.param( 117 | 'from six . moves import map', (3,), '', 118 | id='weird whitespace in dotted name', 119 | ), 120 | pytest.param( 121 | 'from io import open, BytesIO as BIO\n' 122 | 'from io import BytesIO as BIO, open\n', 123 | (3,), 124 | 'from io import BytesIO as BIO\n' 125 | 'from io import BytesIO as BIO\n', 126 | id='removal with import-as', 127 | ), 128 | ), 129 | ) 130 | def test_import_removals(s, min_version, expected): 131 | ret = _fix_plugins(s, settings=Settings(min_version=min_version)) 132 | assert ret == expected 133 | -------------------------------------------------------------------------------- /tests/features/io_open_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from pyupgrade._data import Settings 6 | from pyupgrade._main import _fix_plugins 7 | 8 | 9 | def test_fix_io_open_noop(): 10 | src = '''\ 11 | from io import open 12 | with open("f.txt") as f: 13 | print(f.read()) 14 | ''' 15 | expected = '''\ 16 | with open("f.txt") as f: 17 | print(f.read()) 18 | ''' 19 | ret = _fix_plugins(src, settings=Settings()) 20 | assert ret == expected 21 | 22 | 23 | @pytest.mark.parametrize( 24 | ('s', 'expected'), 25 | ( 26 | ( 27 | 'import io\n\n' 28 | 'with io.open("f.txt", mode="r", buffering=-1, **kwargs) as f:\n' 29 | ' print(f.read())\n', 30 | 31 | 'import io\n\n' 32 | 'with open("f.txt", mode="r", buffering=-1, **kwargs) as f:\n' 33 | ' print(f.read())\n', 34 | ), 35 | ), 36 | ) 37 | def test_fix_io_open(s, expected): 38 | ret = _fix_plugins(s, settings=Settings()) 39 | assert ret == expected 40 | -------------------------------------------------------------------------------- /tests/features/lru_cache_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from pyupgrade._data import Settings 6 | from pyupgrade._main import _fix_plugins 7 | 8 | 9 | @pytest.mark.parametrize( 10 | ('s', 'min_version'), 11 | ( 12 | pytest.param( 13 | 'from functools import lru_cache as lru_cache2\n\n' 14 | '@lru_cache2()\n' 15 | 'def foo():\n' 16 | ' pass\n', 17 | (3, 8), 18 | id='not following as imports', 19 | ), 20 | pytest.param( 21 | 'from functools import lru_cache\n\n' 22 | '@lru_cache(max_size=1024)\n' 23 | 'def foo():\n' 24 | ' pass\n', 25 | (3, 8), 26 | id='not rewriting calls with args', 27 | ), 28 | pytest.param( 29 | 'from functools2 import lru_cache\n\n' 30 | '@lru_cache()\n' 31 | 'def foo():\n' 32 | ' pass\n', 33 | (3, 8), 34 | id='not following unknown import', 35 | ), 36 | pytest.param( 37 | 'from functools import lru_cache\n\n' 38 | '@lru_cache()\n' 39 | 'def foo():\n' 40 | ' pass\n', 41 | (3,), 42 | id='not rewriting below 3.8', 43 | ), 44 | pytest.param( 45 | 'from .functools import lru_cache\n' 46 | '@lru_cache()\n' 47 | 'def foo(): pass\n', 48 | (3, 8), 49 | id='relative imports', 50 | ), 51 | ), 52 | ) 53 | def test_fix_no_arg_decorators_noop(s, min_version): 54 | assert _fix_plugins(s, settings=Settings(min_version=min_version)) == s 55 | 56 | 57 | @pytest.mark.parametrize( 58 | ('s', 'expected'), 59 | ( 60 | pytest.param( 61 | 'from functools import lru_cache\n\n' 62 | '@lru_cache()\n' 63 | 'def foo():\n' 64 | ' pass\n', 65 | 'from functools import lru_cache\n\n' 66 | '@lru_cache\n' 67 | 'def foo():\n' 68 | ' pass\n', 69 | id='call without attr', 70 | ), 71 | pytest.param( 72 | 'import functools\n\n' 73 | '@functools.lru_cache()\n' 74 | 'def foo():\n' 75 | ' pass\n', 76 | 'import functools\n\n' 77 | '@functools.lru_cache\n' 78 | 'def foo():\n' 79 | ' pass\n', 80 | id='call with attr', 81 | ), 82 | ), 83 | ) 84 | def test_fix_no_arg_decorators(s, expected): 85 | ret = _fix_plugins(s, settings=Settings(min_version=(3, 8))) 86 | assert ret == expected 87 | 88 | 89 | @pytest.mark.parametrize( 90 | ('s', 'min_version'), 91 | ( 92 | pytest.param( 93 | 'from functools import lru_cache\n' 94 | '@lru_cache(maxsize=None)\n' 95 | 'def foo(): pass\n', 96 | (3, 9), 97 | id='from imported', 98 | ), 99 | pytest.param( 100 | 'from functools import lru_cache\n' 101 | '@lru_cache(maxsize=1024)\n' 102 | 'def foo(): pass\n', 103 | (3, 9), 104 | id='unrelated parameter', 105 | ), 106 | pytest.param( 107 | 'import functools\n\n' 108 | '@functools.lru_cache(maxsize=None, typed=True)\n' 109 | 'def foo():\n' 110 | ' pass\n', 111 | (3, 9), 112 | id='typed=True', 113 | ), 114 | pytest.param( 115 | 'import functools\n\n' 116 | '@functools.lru_cache(maxsize=None, typed=False, foo=False)\n' 117 | 'def foo():\n' 118 | ' pass\n', 119 | (3, 9), 120 | id='invalid keyword', 121 | ), 122 | ), 123 | ) 124 | def test_fix_maxsize_none_decorators_noop(s, min_version): 125 | assert _fix_plugins(s, settings=Settings(min_version=min_version)) == s 126 | 127 | 128 | @pytest.mark.parametrize( 129 | ('s', 'expected'), 130 | ( 131 | pytest.param( 132 | 'import functools\n\n' 133 | '@functools.lru_cache(maxsize=None)\n' 134 | 'def foo():\n' 135 | ' pass\n', 136 | 'import functools\n\n' 137 | '@functools.cache\n' 138 | 'def foo():\n' 139 | ' pass\n', 140 | id='call with attr', 141 | ), 142 | pytest.param( 143 | 'import functools\n\n' 144 | '@functools.lru_cache(maxsize=None, typed=False)\n' 145 | 'def foo():\n' 146 | ' pass\n', 147 | 'import functools\n\n' 148 | '@functools.cache\n' 149 | 'def foo():\n' 150 | ' pass\n', 151 | id='call with attr, maxsize=None then typed=False', 152 | ), 153 | pytest.param( 154 | 'import functools\n\n' 155 | '@functools.lru_cache(typed=False, maxsize=None)\n' 156 | 'def foo():\n' 157 | ' pass\n', 158 | 'import functools\n\n' 159 | '@functools.cache\n' 160 | 'def foo():\n' 161 | ' pass\n', 162 | id='call with attr, typed=False then maxsize=None', 163 | ), 164 | ), 165 | ) 166 | def test_fix_maxsize_none_decorators(s, expected): 167 | ret = _fix_plugins(s, settings=Settings(min_version=(3, 9))) 168 | assert ret == expected 169 | -------------------------------------------------------------------------------- /tests/features/metaclass_type_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from pyupgrade._data import Settings 6 | from pyupgrade._main import _fix_plugins 7 | 8 | 9 | @pytest.mark.parametrize( 10 | 's', 11 | ( 12 | pytest.param( 13 | 'x = type\n' 14 | '__metaclass__ = x\n', 15 | id='not rewriting "type" rename', 16 | ), 17 | pytest.param( 18 | 'def foo():\n' 19 | ' __metaclass__ = type\n', 20 | id='not rewriting function scope', 21 | ), 22 | pytest.param( 23 | 'class Foo:\n' 24 | ' __metaclass__ = type\n', 25 | id='not rewriting class scope', 26 | ), 27 | pytest.param( 28 | '__metaclass__, __meta_metaclass__ = type, None\n', 29 | id='not rewriting multiple assignment', 30 | ), 31 | ), 32 | ) 33 | def test_metaclass_type_assignment_noop(s): 34 | assert _fix_plugins(s, settings=Settings()) == s 35 | 36 | 37 | @pytest.mark.parametrize( 38 | ('s', 'expected'), 39 | ( 40 | pytest.param( 41 | '__metaclass__ = type', 42 | '', 43 | id='module-scope assignment', 44 | ), 45 | pytest.param( 46 | '__metaclass__ = type', 47 | '', 48 | id='module-scope assignment with extra whitespace', 49 | ), 50 | pytest.param( 51 | '__metaclass__ = (\n' 52 | ' type\n' 53 | ')\n', 54 | '', 55 | id='module-scope assignment across newline', 56 | ), 57 | pytest.param( 58 | '__metaclass__ = type\n' 59 | 'a = 1\n', 60 | 'a = 1\n', 61 | id='replace with code after it', 62 | ), 63 | ), 64 | ) 65 | def test_fix_metaclass_type_assignment(s, expected): 66 | ret = _fix_plugins(s, settings=Settings()) 67 | assert ret == expected 68 | -------------------------------------------------------------------------------- /tests/features/mock_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from pyupgrade._data import Settings 6 | from pyupgrade._main import _fix_plugins 7 | 8 | 9 | @pytest.mark.parametrize( 10 | ('s', 'expected'), 11 | ( 12 | pytest.param( 13 | 'import mock.mock\n' 14 | '\n' 15 | 'mock.mock.patch("func1")\n' 16 | 'mock.patch("func2")\n', 17 | 'from unittest import mock\n' 18 | '\n' 19 | 'mock.patch("func1")\n' 20 | 'mock.patch("func2")\n', 21 | id='double mock absolute import func', 22 | ), 23 | pytest.param( 24 | 'import mock.mock\n' 25 | '\n' 26 | 'mock.mock.patch.object(Foo, "func1")\n' 27 | 'mock.patch.object(Foo, "func2")\n', 28 | 'from unittest import mock\n' 29 | '\n' 30 | 'mock.patch.object(Foo, "func1")\n' 31 | 'mock.patch.object(Foo, "func2")\n', 32 | id='double mock absolute import func attr', 33 | ), 34 | ), 35 | ) 36 | def test_fix_mock(s, expected): 37 | assert _fix_plugins(s, settings=Settings()) == expected 38 | -------------------------------------------------------------------------------- /tests/features/native_literals_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from pyupgrade._data import Settings 6 | from pyupgrade._main import _fix_plugins 7 | 8 | 9 | @pytest.mark.parametrize( 10 | 's', 11 | ( 12 | 'str(1)', 13 | 'str("foo"\n"bar")', # creates a syntax error 14 | 'str(*a)', 'str("foo", *a)', 15 | 'str(**k)', 'str("foo", **k)', 16 | 'str("foo", encoding="UTF-8")', 17 | 'bytes("foo", encoding="UTF-8")', 18 | 'bytes(b"foo"\nb"bar")', 19 | 'bytes("foo"\n"bar")', 20 | 'bytes(*a)', 'bytes("foo", *a)', 21 | 'bytes("foo", **a)', 22 | ), 23 | ) 24 | def test_fix_native_literals_noop(s): 25 | assert _fix_plugins(s, settings=Settings()) == s 26 | 27 | 28 | @pytest.mark.parametrize( 29 | ('s', 'expected'), 30 | ( 31 | ('str()', "''"), 32 | ('str("foo")', '"foo"'), 33 | ('str("""\nfoo""")', '"""\nfoo"""'), 34 | ('six.ensure_str("foo")', '"foo"'), 35 | ('six.ensure_text("foo")', '"foo"'), 36 | ('six.text_type("foo")', '"foo"'), 37 | pytest.param( 38 | 'from six import text_type\n' 39 | 'text_type("foo")\n', 40 | 41 | 'from six import text_type\n' 42 | '"foo"\n', 43 | 44 | id='from import of rewritten name', 45 | ), 46 | ('bytes()', "b''"), 47 | ('bytes(b"foo")', 'b"foo"'), 48 | ('bytes(b"""\nfoo""")', 'b"""\nfoo"""'), 49 | ), 50 | ) 51 | def test_fix_native_literals(s, expected): 52 | ret = _fix_plugins(s, settings=Settings()) 53 | assert ret == expected 54 | -------------------------------------------------------------------------------- /tests/features/new_style_classes_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from pyupgrade._data import Settings 6 | from pyupgrade._main import _fix_plugins 7 | 8 | 9 | @pytest.mark.parametrize( 10 | 's', 11 | ( 12 | # syntax error 13 | 'x = (', 14 | # does not inherit from `object` 15 | 'class C(B): pass', 16 | ), 17 | ) 18 | def test_fix_classes_noop(s): 19 | assert _fix_plugins(s, settings=Settings()) == s 20 | 21 | 22 | @pytest.mark.parametrize( 23 | ('s', 'expected'), 24 | ( 25 | ( 26 | 'class C(object): pass', 27 | 'class C: pass', 28 | ), 29 | ( 30 | 'class C(\n' 31 | ' object,\n' 32 | '): pass', 33 | 'class C: pass', 34 | ), 35 | ( 36 | 'class C(B, object): pass', 37 | 'class C(B): pass', 38 | ), 39 | ( 40 | 'class C(B, (object)): pass', 41 | 'class C(B): pass', 42 | ), 43 | ( 44 | 'class C(B, ( object )): pass', 45 | 'class C(B): pass', 46 | ), 47 | ( 48 | 'class C((object)): pass', 49 | 'class C: pass', 50 | ), 51 | ( 52 | 'class C(\n' 53 | ' B,\n' 54 | ' object,\n' 55 | '): pass\n', 56 | 'class C(\n' 57 | ' B,\n' 58 | '): pass\n', 59 | ), 60 | ( 61 | 'class C(\n' 62 | ' B,\n' 63 | ' object\n' 64 | '): pass\n', 65 | 'class C(\n' 66 | ' B\n' 67 | '): pass\n', 68 | ), 69 | # only legal in python2 70 | ( 71 | 'class C(object, B): pass', 72 | 'class C(B): pass', 73 | ), 74 | ( 75 | 'class C((object), B): pass', 76 | 'class C(B): pass', 77 | ), 78 | ( 79 | 'class C(( object ), B): pass', 80 | 'class C(B): pass', 81 | ), 82 | ( 83 | 'class C(\n' 84 | ' object,\n' 85 | ' B,\n' 86 | '): pass', 87 | 'class C(\n' 88 | ' B,\n' 89 | '): pass', 90 | ), 91 | ( 92 | 'class C(\n' 93 | ' object, # comment!\n' 94 | ' B,\n' 95 | '): pass', 96 | 'class C(\n' 97 | ' B,\n' 98 | '): pass', 99 | ), 100 | ( 101 | 'class C(object, metaclass=ABCMeta): pass', 102 | 'class C(metaclass=ABCMeta): pass', 103 | ), 104 | ), 105 | ) 106 | def test_fix_classes(s, expected): 107 | ret = _fix_plugins(s, settings=Settings()) 108 | assert ret == expected 109 | -------------------------------------------------------------------------------- /tests/features/open_mode_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from pyupgrade._data import Settings 6 | from pyupgrade._main import _fix_plugins 7 | from pyupgrade._plugins.open_mode import _permute 8 | from pyupgrade._plugins.open_mode import _plus 9 | 10 | 11 | def test_plus(): 12 | assert _plus(('a',)) == ('a', 'a+') 13 | assert _plus(('a', 'b')) == ('a', 'b', 'a+', 'b+') 14 | 15 | 16 | def test_permute(): 17 | assert _permute('ab') == ('ab', 'ba') 18 | assert _permute('abc') == ('abc', 'acb', 'bac', 'bca', 'cab', 'cba') 19 | 20 | 21 | @pytest.mark.parametrize( 22 | 's', 23 | ( 24 | # already a reduced mode 25 | 'open("foo", "w")', 26 | 'open("foo", mode="w")', 27 | 'open("foo", "rb")', 28 | # nonsense mode 29 | 'open("foo", "Uw")', 30 | 'open("foo", qux="r")', 31 | 'open("foo", 3)', 32 | 'open(mode="r")', 33 | # don't remove this, they meant to use `encoding=` 34 | 'open("foo", "r", "utf-8")', 35 | ), 36 | ) 37 | def test_fix_open_mode_noop(s): 38 | assert _fix_plugins(s, settings=Settings()) == s 39 | 40 | 41 | @pytest.mark.parametrize( 42 | ('s', 'expected'), 43 | ( 44 | ('open("foo", "U")', 'open("foo")'), 45 | ('open("foo", mode="U")', 'open("foo")'), 46 | ('open("foo", "Ur")', 'open("foo")'), 47 | ('open("foo", mode="Ur")', 'open("foo")'), 48 | ('open("foo", "Ub")', 'open("foo", "rb")'), 49 | ('open("foo", mode="Ub")', 'open("foo", mode="rb")'), 50 | ('open("foo", "rUb")', 'open("foo", "rb")'), 51 | ('open("foo", mode="rUb")', 'open("foo", mode="rb")'), 52 | ('open("foo", "r")', 'open("foo")'), 53 | ('open("foo", mode="r")', 'open("foo")'), 54 | ('open("foo", "rt")', 'open("foo")'), 55 | ('open("foo", mode="rt")', 'open("foo")'), 56 | ('open("f", "r", encoding="UTF-8")', 'open("f", encoding="UTF-8")'), 57 | ( 58 | 'open("f", mode="r", encoding="UTF-8")', 59 | 'open("f", encoding="UTF-8")', 60 | ), 61 | ( 62 | 'open(file="f", mode="r", encoding="UTF-8")', 63 | 'open(file="f", encoding="UTF-8")', 64 | ), 65 | ( 66 | 'open("f", encoding="UTF-8", mode="r")', 67 | 'open("f", encoding="UTF-8")', 68 | ), 69 | ( 70 | 'open(file="f", encoding="UTF-8", mode="r")', 71 | 'open(file="f", encoding="UTF-8")', 72 | ), 73 | ( 74 | 'open(mode="r", encoding="UTF-8", file="t.py")', 75 | 'open(encoding="UTF-8", file="t.py")', 76 | ), 77 | pytest.param('open(f, u"r")', 'open(f)', id='string with u flag'), 78 | pytest.param( 79 | 'io.open("foo", "r")', 80 | 'open("foo")', 81 | id='io.open also rewrites modes in a single pass', 82 | ), 83 | ('open("foo", "wt")', 'open("foo", "w")'), 84 | ('open("foo", "xt")', 'open("foo", "x")'), 85 | ('open("foo", "at")', 'open("foo", "a")'), 86 | ('open("foo", "wt+")', 'open("foo", "w+")'), 87 | ('open("foo", "rt+")', 'open("foo", "r+")'), 88 | ), 89 | ) 90 | def test_fix_open_mode(s, expected): 91 | ret = _fix_plugins(s, settings=Settings()) 92 | assert ret == expected 93 | -------------------------------------------------------------------------------- /tests/features/set_literals_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from pyupgrade._data import Settings 6 | from pyupgrade._main import _fix_plugins 7 | 8 | 9 | @pytest.mark.parametrize( 10 | 's', 11 | ( 12 | # Don't touch empty set literals 13 | 'set()', 14 | # Don't touch weird looking function calls -- use autopep8 or such 15 | # first 16 | 'set ((1, 2))', 17 | pytest.param( 18 | 'f"{set((1, 2))}"', 19 | id='set directly inside f-string placeholder', 20 | ), 21 | pytest.param( 22 | 'f"{set(x for x in y)}"', 23 | id='set comp directly inside f-string placeholder', 24 | ), 25 | ), 26 | ) 27 | def test_fix_sets_noop(s): 28 | assert _fix_plugins(s, settings=Settings()) == s 29 | 30 | 31 | @pytest.mark.parametrize( 32 | ('s', 'expected'), 33 | ( 34 | # Take a set literal with an empty tuple / list and remove the arg 35 | ('set(())', 'set()'), 36 | ('set([])', 'set()'), 37 | pytest.param('set (())', 'set ()', id='empty, weird ws'), 38 | # Remove spaces in empty set literals 39 | ('set(( ))', 'set()'), 40 | # Some "normal" test cases 41 | ('set((1, 2))', '{1, 2}'), 42 | ('set([1, 2])', '{1, 2}'), 43 | ('set(x for x in y)', '{x for x in y}'), 44 | ('set([x for x in y])', '{x for x in y}'), 45 | # These are strange cases -- the ast doesn't tell us about the parens 46 | # here so we have to parse ourselves 47 | ('set((x for x in y))', '{x for x in y}'), 48 | ('set(((1, 2)))', '{1, 2}'), 49 | # The ast also doesn't tell us about the start of the tuple in this 50 | # generator expression 51 | ('set((a, b) for a, b in y)', '{(a, b) for a, b in y}'), 52 | # The ast also doesn't tell us about the start of the tuple for 53 | # tuple of tuples 54 | ('set(((1, 2), (3, 4)))', '{(1, 2), (3, 4)}'), 55 | # Lists where the first element is a tuple also gives the ast trouble 56 | # The first element lies about the offset of the element 57 | ('set([(1, 2), (3, 4)])', '{(1, 2), (3, 4)}'), 58 | ( 59 | 'set(\n' 60 | ' [(1, 2)]\n' 61 | ')', 62 | '{\n' 63 | ' (1, 2)\n' 64 | '}', 65 | ), 66 | ('set([((1, 2)), (3, 4)])', '{((1, 2)), (3, 4)}'), 67 | # And it gets worse 68 | ('set((((1, 2),),))', '{((1, 2),)}'), 69 | # Some multiline cases 70 | ('set(\n(1, 2))', '{\n1, 2}'), 71 | ('set((\n1,\n2,\n))\n', '{\n1,\n2,\n}\n'), 72 | # Nested sets 73 | ( 74 | 'set((frozenset(set((1, 2))), frozenset(set((3, 4)))))', 75 | '{frozenset({1, 2}), frozenset({3, 4})}', 76 | ), 77 | # Remove trailing commas on inline things 78 | ('set((1,))', '{1}'), 79 | ('set((1, ))', '{1}'), 80 | # Remove trailing commas after things 81 | ('set([1, 2, 3,],)', '{1, 2, 3}'), 82 | ('set((x for x in y),)', '{x for x in y}'), 83 | ( 84 | 'set(\n' 85 | ' (x for x in y),\n' 86 | ')', 87 | '{\n' 88 | ' x for x in y\n' 89 | '}', 90 | ), 91 | ( 92 | 'set(\n' 93 | ' [\n' 94 | ' 99, 100,\n' 95 | ' ],\n' 96 | ')\n', 97 | '{\n' 98 | ' 99, 100,\n' 99 | '}\n', 100 | ), 101 | pytest.param('set((\n))', 'set()', id='empty literal with newline'), 102 | pytest.param( 103 | 'set((f"{x}(",))', 104 | '{f"{x}("}', 105 | id='3.12 fstring containing open brace', 106 | ), 107 | pytest.param( 108 | 'set((f"{x})",))', 109 | '{f"{x})"}', 110 | id='3.12 fstring containing close brace', 111 | ), 112 | ), 113 | ) 114 | def test_sets(s, expected): 115 | ret = _fix_plugins(s, settings=Settings()) 116 | assert ret == expected 117 | -------------------------------------------------------------------------------- /tests/features/shlex_join_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from pyupgrade._data import Settings 6 | from pyupgrade._main import _fix_plugins 7 | 8 | 9 | @pytest.mark.parametrize( 10 | ('s', 'version'), 11 | ( 12 | pytest.param( 13 | 'from shlex import quote\n' 14 | '" ".join(quote(arg) for arg in cmd)\n', 15 | (3, 8), 16 | id='quote from-imported', 17 | ), 18 | pytest.param( 19 | 'import shlex\n' 20 | '"wat".join(shlex.quote(arg) for arg in cmd)\n', 21 | (3, 8), 22 | id='not joined with space', 23 | ), 24 | pytest.param( 25 | 'import shlex\n' 26 | '" ".join(shlex.quote(arg) for arg in cmd)\n', 27 | (3, 7), 28 | id='3.8+ feature', 29 | ), 30 | ), 31 | ) 32 | def test_shlex_join_noop(s, version): 33 | assert _fix_plugins(s, settings=Settings(min_version=version)) == s 34 | 35 | 36 | @pytest.mark.parametrize( 37 | ('s', 'expected'), 38 | ( 39 | pytest.param( 40 | 'import shlex\n' 41 | '" ".join(shlex.quote(arg) for arg in cmd)\n', 42 | 43 | 'import shlex\n' 44 | 'shlex.join(cmd)\n', 45 | 46 | id='generator expression', 47 | ), 48 | pytest.param( 49 | 'import shlex\n' 50 | '" ".join([shlex.quote(arg) for arg in cmd])\n', 51 | 52 | 'import shlex\n' 53 | 'shlex.join(cmd)\n', 54 | 55 | id='list comprehension', 56 | ), 57 | pytest.param( 58 | 'import shlex\n' 59 | '" ".join([shlex.quote(arg) for arg in cmd],)\n', 60 | 61 | 'import shlex\n' 62 | 'shlex.join(cmd)\n', 63 | 64 | id='removes trailing comma', 65 | ), 66 | pytest.param( 67 | 'import shlex\n' 68 | '" ".join([shlex.quote(arg) for arg in ["a", "b", "c"]],)\n', 69 | 70 | 'import shlex\n' 71 | 'shlex.join(["a", "b", "c"])\n', 72 | 73 | id='more complicated iterable', 74 | ), 75 | ), 76 | ) 77 | def test_shlex_join_fixes(s, expected): 78 | assert _fix_plugins(s, settings=Settings(min_version=(3, 8))) == expected 79 | -------------------------------------------------------------------------------- /tests/features/six_b_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from pyupgrade._data import Settings 6 | from pyupgrade._main import _fix_plugins 7 | 8 | 9 | @pytest.mark.parametrize( 10 | 's', 11 | ( 12 | # non-ascii bytestring 13 | 'print(six.b("£"))', 14 | # extra whitespace 15 | 'print(six.b( "123"))', 16 | # cannot determine args to rewrite them 17 | 'six.b(*a)', 18 | ), 19 | ) 20 | def test_six_b_noop(s): 21 | assert _fix_plugins(s, settings=Settings()) == s 22 | 23 | 24 | @pytest.mark.parametrize( 25 | ('s', 'expected'), 26 | ( 27 | ( 28 | 'six.b("123")', 29 | 'b"123"', 30 | ), 31 | ( 32 | 'six.b(r"123")', 33 | 'br"123"', 34 | ), 35 | ( 36 | r'six.b("\x12\xef")', 37 | r'b"\x12\xef"', 38 | ), 39 | ( 40 | 'six.ensure_binary("foo")', 41 | 'b"foo"', 42 | ), 43 | ( 44 | 'from six import b\n\n' r'b("\x12\xef")', 45 | 'from six import b\n\n' r'b"\x12\xef"', 46 | ), 47 | ), 48 | ) 49 | def test_six_b(s, expected): 50 | ret = _fix_plugins(s, settings=Settings()) 51 | assert ret == expected 52 | -------------------------------------------------------------------------------- /tests/features/six_remove_decorators_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from pyupgrade._data import Settings 6 | from pyupgrade._main import _fix_plugins 7 | 8 | 9 | @pytest.mark.parametrize( 10 | ('s', 'expected'), 11 | ( 12 | ( 13 | '@six.python_2_unicode_compatible\n' 14 | 'class C: pass', 15 | 16 | 'class C: pass', 17 | ), 18 | ( 19 | '@six.python_2_unicode_compatible\n' 20 | '@other_decorator\n' 21 | 'class C: pass', 22 | 23 | '@other_decorator\n' 24 | 'class C: pass', 25 | ), 26 | pytest.param( 27 | '@ six.python_2_unicode_compatible\n' 28 | 'class C: pass\n', 29 | 30 | 'class C: pass\n', 31 | 32 | id='weird spacing at the beginning python_2_unicode_compatible', 33 | ), 34 | ( 35 | 'from six import python_2_unicode_compatible\n' 36 | '@python_2_unicode_compatible\n' 37 | 'class C: pass', 38 | 39 | 'from six import python_2_unicode_compatible\n' 40 | 'class C: pass', 41 | ), 42 | ), 43 | ) 44 | def test_fix_six_remove_decorators(s, expected): 45 | ret = _fix_plugins(s, settings=Settings()) 46 | assert ret == expected 47 | -------------------------------------------------------------------------------- /tests/features/six_simple_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from pyupgrade._data import Settings 6 | from pyupgrade._main import _fix_plugins 7 | 8 | 9 | @pytest.mark.parametrize( 10 | 's', 11 | ( 12 | # renaming things for weird reasons 13 | 'from six import MAXSIZE as text_type\n' 14 | 'isinstance(s, text_type)\n', 15 | # parenthesized part of attribute 16 | '(\n' 17 | ' six\n' 18 | ').text_type(u)\n', 19 | pytest.param( 20 | 'from .six import text_type\n' 21 | 'isinstance("foo", text_type)\n', 22 | id='relative import might not be six', 23 | ), 24 | pytest.param( 25 | 'foo.range(3)', 26 | id='Range, but not from six.moves', 27 | ), 28 | ), 29 | ) 30 | def test_six_simple_noop(s): 31 | assert _fix_plugins(s, settings=Settings()) == s 32 | 33 | 34 | @pytest.mark.parametrize( 35 | ('s', 'expected'), 36 | ( 37 | ( 38 | 'isinstance(s, six.text_type)', 39 | 'isinstance(s, str)', 40 | ), 41 | pytest.param( 42 | 'isinstance(s, six . string_types)', 43 | 'isinstance(s, str)', 44 | id='weird spacing on six.attr', 45 | ), 46 | ( 47 | 'isinstance(s, six.string_types)', 48 | 'isinstance(s, str)', 49 | ), 50 | ( 51 | 'issubclass(tp, six.string_types)', 52 | 'issubclass(tp, str)', 53 | ), 54 | ( 55 | 'STRING_TYPES = six.string_types', 56 | 'STRING_TYPES = (str,)', 57 | ), 58 | ( 59 | 'from six import string_types\n' 60 | 'isinstance(s, string_types)\n', 61 | 62 | 'from six import string_types\n' 63 | 'isinstance(s, str)\n', 64 | ), 65 | ( 66 | 'from six import string_types\n' 67 | 'STRING_TYPES = string_types\n', 68 | 69 | 'from six import string_types\n' 70 | 'STRING_TYPES = (str,)\n', 71 | ), 72 | pytest.param( 73 | 'six.moves.range(3)\n', 74 | 75 | 'range(3)\n', 76 | 77 | id='six.moves.range', 78 | ), 79 | pytest.param( 80 | 'six.moves.xrange(3)\n', 81 | 82 | 'range(3)\n', 83 | 84 | id='six.moves.xrange', 85 | ), 86 | pytest.param( 87 | 'from six.moves import xrange\n' 88 | 'xrange(3)\n', 89 | 90 | 'from six.moves import xrange\n' 91 | 'range(3)\n', 92 | 93 | id='six.moves.xrange, from import', 94 | ), 95 | ), 96 | ) 97 | def test_fix_six_simple(s, expected): 98 | ret = _fix_plugins(s, settings=Settings()) 99 | assert ret == expected 100 | -------------------------------------------------------------------------------- /tests/features/super_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from pyupgrade._main import _fix_plugins 6 | from pyupgrade._main import Settings 7 | 8 | 9 | @pytest.mark.parametrize( 10 | 's', 11 | ( 12 | # syntax error 13 | 'x(', 14 | 15 | 'class C(Base):\n' 16 | ' def f(self):\n' 17 | ' super().f()\n', 18 | 19 | # super class doesn't match class name 20 | 'class C(Base):\n' 21 | ' def f(self):\n' 22 | ' super(Base, self).f()\n', 23 | 'class Outer:\n' # common nesting 24 | ' class C(Base):\n' 25 | ' def f(self):\n' 26 | ' super(C, self).f()\n', 27 | 'class Outer:\n' # higher levels of nesting 28 | ' class Inner:\n' 29 | ' class C(Base):\n' 30 | ' def f(self):\n' 31 | ' super(Inner.C, self).f()\n', 32 | 'class Outer:\n' # super arg1 nested in unrelated name 33 | ' class C(Base):\n' 34 | ' def f(self):\n' 35 | ' super(some_module.Outer.C, self).f()\n', 36 | 37 | # super outside of a class (technically legal!) 38 | 'def f(self):\n' 39 | ' super(C, self).f()\n', 40 | 41 | # super used in a comprehension 42 | 'class C(Base):\n' 43 | ' def f(self):\n' 44 | ' return [super(C, self).f() for _ in ()]\n', 45 | 'class C(Base):\n' 46 | ' def f(self):\n' 47 | ' return {super(C, self).f() for _ in ()}\n', 48 | 'class C(Base):\n' 49 | ' def f(self):\n' 50 | ' return (super(C, self).f() for _ in ())\n', 51 | 'class C(Base):\n' 52 | ' def f(self):\n' 53 | ' return {True: super(C, self).f() for _ in ()}\n', 54 | # nested comprehension 55 | 'class C(Base):\n' 56 | ' def f(self):\n' 57 | ' return [\n' 58 | ' (\n' 59 | ' [_ for _ in ()],\n' 60 | ' super(C, self).f(),\n' 61 | ' )\n' 62 | ' for _ in ()' 63 | ' ]\n', 64 | # super in a closure 65 | 'class C(Base):\n' 66 | ' def f(self):\n' 67 | ' def g():\n' 68 | ' super(C, self).f()\n' 69 | ' g()\n', 70 | 'class C(Base):\n' 71 | ' def f(self):\n' 72 | ' g = lambda: super(C, self).f()\n' 73 | ' g()\n', 74 | ), 75 | ) 76 | def test_fix_super_noop(s): 77 | assert _fix_plugins(s, settings=Settings()) == s 78 | 79 | 80 | @pytest.mark.parametrize( 81 | ('s', 'expected'), 82 | ( 83 | ( 84 | 'class C(Base):\n' 85 | ' def f(self):\n' 86 | ' super(C, self).f()\n', 87 | 'class C(Base):\n' 88 | ' def f(self):\n' 89 | ' super().f()\n', 90 | ), 91 | ( 92 | 'class C(Base):\n' 93 | ' def f(self):\n' 94 | ' super (C, self).f()\n', 95 | 'class C(Base):\n' 96 | ' def f(self):\n' 97 | ' super().f()\n', 98 | ), 99 | ( 100 | 'class Outer:\n' 101 | ' class C(Base):\n' 102 | ' def f(self):\n' 103 | ' super (Outer.C, self).f()\n', 104 | 'class Outer:\n' 105 | ' class C(Base):\n' 106 | ' def f(self):\n' 107 | ' super().f()\n', 108 | ), 109 | ( 110 | 'def f():\n' 111 | ' class Outer:\n' 112 | ' class C(Base):\n' 113 | ' def f(self):\n' 114 | ' super(Outer.C, self).f()\n', 115 | 'def f():\n' 116 | ' class Outer:\n' 117 | ' class C(Base):\n' 118 | ' def f(self):\n' 119 | ' super().f()\n', 120 | ), 121 | ( 122 | 'class A:\n' 123 | ' class B:\n' 124 | ' class C:\n' 125 | ' def f(self):\n' 126 | ' super(A.B.C, self).f()\n', 127 | 'class A:\n' 128 | ' class B:\n' 129 | ' class C:\n' 130 | ' def f(self):\n' 131 | ' super().f()\n', 132 | ), 133 | ( 134 | 'class C(Base):\n' 135 | ' f = lambda self: super(C, self).f()\n', 136 | 'class C(Base):\n' 137 | ' f = lambda self: super().f()\n', 138 | ), 139 | ( 140 | 'class C(Base):\n' 141 | ' @classmethod\n' 142 | ' def f(cls):\n' 143 | ' super(C, cls).f()\n', 144 | 'class C(Base):\n' 145 | ' @classmethod\n' 146 | ' def f(cls):\n' 147 | ' super().f()\n', 148 | ), 149 | pytest.param( 150 | 'class C:\n' 151 | ' async def foo(self):\n' 152 | ' super(C, self).foo()\n', 153 | 154 | 'class C:\n' 155 | ' async def foo(self):\n' 156 | ' super().foo()\n', 157 | 158 | id='async def super', 159 | ), 160 | ), 161 | ) 162 | def test_fix_super(s, expected): 163 | assert _fix_plugins(s, settings=Settings()) == expected 164 | -------------------------------------------------------------------------------- /tests/features/type_of_primitive_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from pyupgrade._data import Settings 6 | from pyupgrade._main import _fix_plugins 7 | 8 | 9 | @pytest.mark.parametrize( 10 | 's', 11 | ( 12 | pytest.param( 13 | 'type(None)\n', 14 | id='NoneType', 15 | ), 16 | pytest.param( 17 | 'type(...)\n', 18 | id='ellipsis', 19 | ), 20 | pytest.param( 21 | 'foo = "foo"\n' 22 | 'type(foo)\n', 23 | id='String assigned to variable', 24 | ), 25 | ), 26 | ) 27 | def test_fix_type_of_primitive_noop(s): 28 | assert _fix_plugins(s, settings=Settings()) == s 29 | 30 | 31 | @pytest.mark.parametrize( 32 | ('s', 'expected'), 33 | ( 34 | pytest.param( 35 | 'type("")\n', 36 | 37 | 'str\n', 38 | 39 | id='Empty string -> str', 40 | ), 41 | pytest.param( 42 | 'type(0)\n', 43 | 44 | 'int\n', 45 | 46 | id='zero -> int', 47 | ), 48 | pytest.param( 49 | 'type(0.)\n', 50 | 51 | 'float\n', 52 | 53 | id='decimal zero -> float', 54 | ), 55 | pytest.param( 56 | 'type(0j)\n', 57 | 58 | 'complex\n', 59 | 60 | id='0j -> complex', 61 | ), 62 | pytest.param( 63 | 'type(b"")\n', 64 | 65 | 'bytes\n', 66 | 67 | id='Empty bytes string -> bytes', 68 | ), 69 | pytest.param( 70 | 'type(True)\n', 71 | 72 | 'bool\n', 73 | 74 | id='bool', 75 | ), 76 | ), 77 | ) 78 | def test_fix_type_of_primitive(s, expected): 79 | ret = _fix_plugins(s, settings=Settings()) 80 | assert ret == expected 81 | -------------------------------------------------------------------------------- /tests/features/typing_pep585_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from pyupgrade._data import Settings 6 | from pyupgrade._main import _fix_plugins 7 | 8 | 9 | @pytest.mark.parametrize( 10 | ('s', 'version'), 11 | ( 12 | pytest.param( 13 | 'x = lambda foo: None', 14 | (3, 9), 15 | id='lambdas do not have type annotations', 16 | ), 17 | pytest.param( 18 | 'from typing import List\n' 19 | 'x: List[int]\n', 20 | (3, 8), 21 | id='not python 3.9+', 22 | ), 23 | pytest.param( 24 | 'from __future__ import annotations\n' 25 | 'from typing import List\n' 26 | 'SomeAlias = List[int]\n', 27 | (3, 8), 28 | id='not in a type annotation context', 29 | ), 30 | pytest.param( 31 | 'from typing import Union\n' 32 | 'x: Union[int, str]\n', 33 | (3, 9), 34 | id='not a PEP 585 type', 35 | ), 36 | ), 37 | ) 38 | def test_fix_generic_types_noop(s, version): 39 | assert _fix_plugins(s, settings=Settings(min_version=version)) == s 40 | 41 | 42 | def test_noop_keep_runtime_typing(): 43 | s = '''\ 44 | from __future__ import annotations 45 | from typing import List 46 | def f(x: List[str]) -> None: ... 47 | ''' 48 | assert _fix_plugins(s, settings=Settings(keep_runtime_typing=True)) == s 49 | 50 | 51 | def test_keep_runtime_typing_ignored_in_py39(): 52 | s = '''\ 53 | from __future__ import annotations 54 | from typing import List 55 | def f(x: List[str]) -> None: ... 56 | ''' 57 | expected = '''\ 58 | from __future__ import annotations 59 | from typing import List 60 | def f(x: list[str]) -> None: ... 61 | ''' 62 | settings = Settings(min_version=(3, 9), keep_runtime_typing=True) 63 | assert _fix_plugins(s, settings=settings) == expected 64 | 65 | 66 | @pytest.mark.parametrize( 67 | ('s', 'expected'), 68 | ( 69 | pytest.param( 70 | 'from typing import List\n' 71 | 'x: List[int]\n', 72 | 73 | 'from typing import List\n' 74 | 'x: list[int]\n', 75 | 76 | id='from import of List', 77 | ), 78 | pytest.param( 79 | 'import typing\n' 80 | 'x: typing.List[int]\n', 81 | 82 | 'import typing\n' 83 | 'x: list[int]\n', 84 | 85 | id='import of typing + typing.List', 86 | ), 87 | pytest.param( 88 | 'from typing import List\n' 89 | 'SomeAlias = List[int]\n', 90 | 'from typing import List\n' 91 | 'SomeAlias = list[int]\n', 92 | id='not in a type annotation context', 93 | ), 94 | ), 95 | ) 96 | def test_fix_generic_types(s, expected): 97 | ret = _fix_plugins(s, settings=Settings(min_version=(3, 9))) 98 | assert ret == expected 99 | 100 | 101 | @pytest.mark.parametrize( 102 | ('s', 'expected'), 103 | ( 104 | pytest.param( 105 | 'from __future__ import annotations\n' 106 | 'from typing import List\n' 107 | 'x: List[int]\n', 108 | 109 | 'from __future__ import annotations\n' 110 | 'from typing import List\n' 111 | 'x: list[int]\n', 112 | 113 | id='variable annotations', 114 | ), 115 | pytest.param( 116 | 'from __future__ import annotations\n' 117 | 'from typing import List\n' 118 | 'def f(x: List[int]) -> None: ...\n', 119 | 120 | 'from __future__ import annotations\n' 121 | 'from typing import List\n' 122 | 'def f(x: list[int]) -> None: ...\n', 123 | 124 | id='argument annotations', 125 | ), 126 | pytest.param( 127 | 'from __future__ import annotations\n' 128 | 'from typing import List\n' 129 | 'def f() -> List[int]: ...\n', 130 | 131 | 'from __future__ import annotations\n' 132 | 'from typing import List\n' 133 | 'def f() -> list[int]: ...\n', 134 | 135 | id='return annotations', 136 | ), 137 | ), 138 | ) 139 | def test_fix_generic_types_future_annotations(s, expected): 140 | ret = _fix_plugins(s, settings=Settings()) 141 | assert ret == expected 142 | -------------------------------------------------------------------------------- /tests/features/typing_pep646_unpack_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from pyupgrade._data import Settings 6 | from pyupgrade._main import _fix_plugins 7 | 8 | 9 | @pytest.mark.parametrize( 10 | ('s',), 11 | ( 12 | pytest.param( 13 | 'from typing import Unpack\n' 14 | 'foo(Unpack())', 15 | id='Not a subscript', 16 | ), 17 | pytest.param( 18 | 'from typing import TypeVarTuple, Unpack\n' 19 | 'Shape = TypeVarTuple("Shape")\n' 20 | 'class Foo(Unpack[Shape]):\n' 21 | ' pass', 22 | id='Not inside a subscript', 23 | ), 24 | pytest.param( 25 | 'from typing import Unpack\n' 26 | 'from typing import TypedDict\n' 27 | 'class D(TypedDict):\n' 28 | ' x: int\n' 29 | 'def f(**kwargs: Unpack[D]) -> None: pass\n', 30 | id='3.12 TypedDict for kwargs', 31 | ), 32 | ), 33 | ) 34 | def test_fix_pep646_noop(s): 35 | assert _fix_plugins(s, settings=Settings(min_version=(3, 11))) == s 36 | assert _fix_plugins(s, settings=Settings(min_version=(3, 10))) == s 37 | 38 | 39 | @pytest.mark.parametrize( 40 | ('s', 'expected'), 41 | ( 42 | ( 43 | 'from typing import Generic, TypeVarTuple, Unpack\n' 44 | "Shape = TypeVarTuple('Shape')\n" 45 | 'class C(Generic[Unpack[Shape]]):\n' 46 | ' pass', 47 | 48 | 'from typing import Generic, TypeVarTuple, Unpack\n' 49 | "Shape = TypeVarTuple('Shape')\n" 50 | 'class C(Generic[*Shape]):\n' 51 | ' pass', 52 | ), 53 | ( 54 | 'from typing import Generic, TypeVarTuple, Unpack\n' 55 | "Shape = TypeVarTuple('Shape')\n" 56 | 'class C(Generic[Unpack [Shape]]):\n' 57 | ' pass', 58 | 59 | 'from typing import Generic, TypeVarTuple, Unpack\n' 60 | "Shape = TypeVarTuple('Shape')\n" 61 | 'class C(Generic[*Shape]):\n' 62 | ' pass', 63 | ), 64 | pytest.param( 65 | 'from typing import Unpack\n' 66 | 'def f(*args: Unpack[tuple[int, ...]]): pass\n', 67 | 68 | 'from typing import Unpack\n' 69 | 'def f(*args: *tuple[int, ...]): pass\n', 70 | 71 | id='Unpack for *args', 72 | ), 73 | ), 74 | ) 75 | def test_typing_unpack(s, expected): 76 | assert _fix_plugins(s, settings=Settings(min_version=(3, 11))) == expected 77 | assert _fix_plugins(s, settings=Settings(min_version=(3, 10))) == s 78 | -------------------------------------------------------------------------------- /tests/features/typing_pep696_typevar_defaults_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from pyupgrade._data import Settings 6 | from pyupgrade._main import _fix_plugins 7 | 8 | 9 | @pytest.mark.parametrize( 10 | ('s', 'version'), 11 | ( 12 | pytest.param( 13 | 'from collections.abc import Generator\n' 14 | 'def f() -> Generator[int, None, None]: yield 1\n', 15 | (3, 12), 16 | id='not 3.13+, no __future__.annotations', 17 | ), 18 | pytest.param( 19 | 'from __future__ import annotations\n' 20 | 'from collections.abc import Generator\n' 21 | 'def f() -> Generator[int]: yield 1\n', 22 | (3, 12), 23 | id='already converted!', 24 | ), 25 | pytest.param( 26 | 'from __future__ import annotations\n' 27 | 'from collections.abc import Generator\n' 28 | 'def f() -> Generator[int, int, None]: yield 1\n' 29 | 'def g() -> Generator[int, int, int]: yield 1\n', 30 | (3, 12), 31 | id='non-None send/return type', 32 | ), 33 | ), 34 | ) 35 | def test_fix_pep696_noop(s, version): 36 | assert _fix_plugins(s, settings=Settings(min_version=version)) == s 37 | 38 | 39 | def test_fix_pep696_noop_keep_runtime_typing(): 40 | settings = Settings(min_version=(3, 12), keep_runtime_typing=True) 41 | s = '''\ 42 | from __future__ import annotations 43 | from collections.abc import Generator 44 | def f() -> Generator[int, None, None]: yield 1 45 | ''' 46 | assert _fix_plugins(s, settings=settings) == s 47 | 48 | 49 | @pytest.mark.parametrize( 50 | ('s', 'expected'), 51 | ( 52 | pytest.param( 53 | 'from __future__ import annotations\n' 54 | 'from typing import Generator\n' 55 | 'def f() -> Generator[int, None, None]: yield 1\n', 56 | 57 | 'from __future__ import annotations\n' 58 | 'from collections.abc import Generator\n' 59 | 'def f() -> Generator[int]: yield 1\n', 60 | 61 | id='typing.Generator', 62 | ), 63 | pytest.param( 64 | 'from __future__ import annotations\n' 65 | 'from typing_extensions import Generator\n' 66 | 'def f() -> Generator[int, None, None]: yield 1\n', 67 | 68 | 'from __future__ import annotations\n' 69 | 'from typing_extensions import Generator\n' 70 | 'def f() -> Generator[int]: yield 1\n', 71 | 72 | id='typing_extensions.Generator', 73 | ), 74 | pytest.param( 75 | 'from __future__ import annotations\n' 76 | 'from collections.abc import Generator\n' 77 | 'def f() -> Generator[int, None, None]: yield 1\n', 78 | 79 | 'from __future__ import annotations\n' 80 | 'from collections.abc import Generator\n' 81 | 'def f() -> Generator[int]: yield 1\n', 82 | 83 | id='collections.abc.Generator', 84 | ), 85 | pytest.param( 86 | 'from __future__ import annotations\n' 87 | 'from collections.abc import AsyncGenerator\n' 88 | 'async def f() -> AsyncGenerator[int, None]: yield 1\n', 89 | 90 | 'from __future__ import annotations\n' 91 | 'from collections.abc import AsyncGenerator\n' 92 | 'async def f() -> AsyncGenerator[int]: yield 1\n', 93 | 94 | id='collections.abc.AsyncGenerator', 95 | ), 96 | ), 97 | ) 98 | def test_fix_pep696_with_future_annotations(s, expected): 99 | assert _fix_plugins(s, settings=Settings(min_version=(3, 12))) == expected 100 | 101 | 102 | @pytest.mark.parametrize( 103 | ('s', 'expected'), 104 | ( 105 | pytest.param( 106 | 'from collections.abc import Generator\n' 107 | 'def f() -> Generator[int, None, None]: yield 1\n', 108 | 109 | 'from collections.abc import Generator\n' 110 | 'def f() -> Generator[int]: yield 1\n', 111 | 112 | id='Generator', 113 | ), 114 | pytest.param( 115 | 'from collections.abc import AsyncGenerator\n' 116 | 'async def f() -> AsyncGenerator[int, None]: yield 1\n', 117 | 118 | 'from collections.abc import AsyncGenerator\n' 119 | 'async def f() -> AsyncGenerator[int]: yield 1\n', 120 | 121 | id='AsyncGenerator', 122 | ), 123 | ), 124 | ) 125 | def test_fix_pep696_with_3_13(s, expected): 126 | assert _fix_plugins(s, settings=Settings(min_version=(3, 13))) == expected 127 | -------------------------------------------------------------------------------- /tests/features/typing_text_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from pyupgrade._data import Settings 6 | from pyupgrade._main import _fix_plugins 7 | 8 | 9 | @pytest.mark.parametrize( 10 | 's', 11 | ( 12 | pytest.param( 13 | 'class Text: ...\n' 14 | 'text = Text()\n', 15 | id='not a type annotation', 16 | ), 17 | ), 18 | ) 19 | def test_fix_typing_text_noop(s): 20 | assert _fix_plugins(s, settings=Settings()) == s 21 | 22 | 23 | @pytest.mark.parametrize( 24 | ('s', 'expected'), 25 | ( 26 | pytest.param( 27 | 'from typing import Text\n' 28 | 'x: Text\n', 29 | 30 | 'from typing import Text\n' 31 | 'x: str\n', 32 | 33 | id='from import of Text', 34 | ), 35 | pytest.param( 36 | 'import typing\n' 37 | 'x: typing.Text\n', 38 | 39 | 'import typing\n' 40 | 'x: str\n', 41 | 42 | id='import of typing + typing.Text', 43 | ), 44 | pytest.param( 45 | 'from typing import Text\n' 46 | 'SomeAlias = Text\n', 47 | 'from typing import Text\n' 48 | 'SomeAlias = str\n', 49 | id='not in a type annotation context', 50 | ), 51 | ), 52 | ) 53 | def test_fix_typing_text(s, expected): 54 | ret = _fix_plugins(s, settings=Settings()) 55 | assert ret == expected 56 | -------------------------------------------------------------------------------- /tests/features/unicode_literals_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from pyupgrade._main import _fix_tokens 6 | 7 | 8 | @pytest.mark.parametrize( 9 | 's', 10 | ( 11 | pytest.param('(', id='syntax errors are unchanged'), 12 | # Regression: string containing newline 13 | pytest.param('"""with newline\n"""', id='string containing newline'), 14 | pytest.param( 15 | 'def f():\n' 16 | ' return"foo"\n', 17 | id='Regression: no space between return and string', 18 | ), 19 | ), 20 | ) 21 | def test_unicode_literals_noop(s): 22 | assert _fix_tokens(s) == s 23 | 24 | 25 | @pytest.mark.parametrize( 26 | ('s', 'expected'), 27 | ( 28 | pytest.param("u''", "''", id='it removes u prefix'), 29 | ), 30 | ) 31 | def test_unicode_literals(s, expected): 32 | assert _fix_tokens(s) == expected 33 | -------------------------------------------------------------------------------- /tests/features/unittest_aliases_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from pyupgrade._data import Settings 6 | from pyupgrade._main import _fix_plugins 7 | 8 | 9 | @pytest.mark.parametrize( 10 | 's', 11 | ( 12 | pytest.param( 13 | 'class ExampleTests:\n' 14 | ' def test_something(self):\n' 15 | ' self.assertEqual(1, 1)\n', 16 | id='not a deprecated alias', 17 | ), 18 | 'unittest.makeSuite(Tests, "arg")', 19 | 'unittest.makeSuite(Tests, prefix="arg")', 20 | ), 21 | ) 22 | def test_fix_unittest_aliases_noop(s): 23 | assert _fix_plugins(s, settings=Settings()) == s 24 | 25 | 26 | @pytest.mark.parametrize( 27 | ('s', 'expected'), 28 | ( 29 | ( 30 | 'class ExampleTests:\n' 31 | ' def test_something(self):\n' 32 | ' self.assertEquals(1, 1)\n', 33 | 34 | 'class ExampleTests:\n' 35 | ' def test_something(self):\n' 36 | ' self.assertEqual(1, 1)\n', 37 | ), 38 | ( 39 | 'class ExampleTests:\n' 40 | ' def test_something(self):\n' 41 | ' self.assertNotEquals(1, 2)\n', 42 | 43 | 'class ExampleTests:\n' 44 | ' def test_something(self):\n' 45 | ' self.assertNotEqual(1, 2)\n', 46 | ), 47 | ), 48 | ) 49 | def test_fix_unittest_aliases(s, expected): 50 | ret = _fix_plugins(s, settings=Settings()) 51 | assert ret == expected 52 | 53 | 54 | @pytest.mark.parametrize( 55 | ('s', 'expected'), 56 | ( 57 | ( 58 | 'unittest.findTestCases(MyTests)', 59 | 'unittest.defaultTestLoader.loadTestsFromModule(MyTests)', 60 | ), 61 | ( 62 | 'unittest.makeSuite(MyTests)', 63 | 'unittest.defaultTestLoader.loadTestsFromTestCase(MyTests)', 64 | ), 65 | ( 66 | 'unittest.getTestCaseNames(MyTests)', 67 | 'unittest.defaultTestLoader.getTestCaseNames(MyTests)', 68 | ), 69 | ), 70 | ) 71 | def test_fix_unittest_aliases_py311(s, expected): 72 | ret = _fix_plugins(s, settings=Settings(min_version=(3, 11))) 73 | assert ret == expected 74 | -------------------------------------------------------------------------------- /tests/features/universal_newlines_to_text_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from pyupgrade._data import Settings 6 | from pyupgrade._main import _fix_plugins 7 | 8 | 9 | @pytest.mark.parametrize( 10 | ('s', 'version'), 11 | ( 12 | pytest.param( 13 | 'import subprocess\n' 14 | 'subprocess.run(["foo"], universal_newlines=True)\n', 15 | (3,), 16 | id='not Python3.7+', 17 | ), 18 | pytest.param( 19 | 'from foo import run\n' 20 | 'run(["foo"], universal_newlines=True)\n', 21 | (3, 7), 22 | id='run imported, but not from subprocess', 23 | ), 24 | pytest.param( 25 | 'from subprocess import run\n' 26 | 'run(["foo"], shell=True)\n', 27 | (3, 7), 28 | id='universal_newlines not used', 29 | ), 30 | pytest.param( 31 | 'import subprocess\n' 32 | 'subprocess.run(\n' 33 | ' ["foo"],\n' 34 | ' text=True,\n' 35 | ' universal_newlines=True\n' 36 | ')\n', 37 | (3, 7), 38 | id='both text and universal_newlines', 39 | ), 40 | pytest.param( 41 | 'import subprocess\n' 42 | 'subprocess.run(\n' 43 | ' ["foo"],\n' 44 | ' universal_newlines=True,\n' 45 | ' **kwargs,\n' 46 | ')\n', 47 | (3, 7), 48 | id='both **kwargs and universal_newlines', 49 | ), 50 | ), 51 | ) 52 | def test_fix_universal_newlines_to_text_noop(s, version): 53 | assert _fix_plugins(s, settings=Settings(min_version=version)) == s 54 | 55 | 56 | @pytest.mark.parametrize( 57 | ('s', 'expected'), 58 | ( 59 | pytest.param( 60 | 'import subprocess\n' 61 | 'subprocess.run(["foo"], universal_newlines=True)\n', 62 | 63 | 'import subprocess\n' 64 | 'subprocess.run(["foo"], text=True)\n', 65 | 66 | id='subprocess.run attribute', 67 | ), 68 | pytest.param( 69 | 'import subprocess\n' 70 | 'subprocess.check_output(["foo"], universal_newlines=True)\n', 71 | 72 | 'import subprocess\n' 73 | 'subprocess.check_output(["foo"], text=True)\n', 74 | 75 | id='subprocess.check_output attribute', 76 | ), 77 | pytest.param( 78 | 'from subprocess import run\n' 79 | 'run(["foo"], universal_newlines=True)\n', 80 | 81 | 'from subprocess import run\n' 82 | 'run(["foo"], text=True)\n', 83 | 84 | id='run imported from subprocess', 85 | ), 86 | pytest.param( 87 | 'from subprocess import run\n' 88 | 'run(["foo"], universal_newlines=universal_newlines)\n', 89 | 90 | 'from subprocess import run\n' 91 | 'run(["foo"], text=universal_newlines)\n', 92 | 93 | id='universal_newlines appears as value', 94 | ), 95 | pytest.param( 96 | 'from subprocess import run\n' 97 | 'run(["foo"], *foo, universal_newlines=universal_newlines)\n', 98 | 99 | 'from subprocess import run\n' 100 | 'run(["foo"], *foo, text=universal_newlines)\n', 101 | 102 | id='with starargs', 103 | ), 104 | ), 105 | ) 106 | def test_fix_universal_newlines_to_text(s, expected): 107 | ret = _fix_plugins(s, settings=Settings(min_version=(3, 7))) 108 | assert ret == expected 109 | -------------------------------------------------------------------------------- /tests/features/unpack_list_comprehension_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from pyupgrade._data import Settings 6 | from pyupgrade._main import _fix_plugins 7 | 8 | 9 | @pytest.mark.parametrize( 10 | 's', 11 | ( 12 | pytest.param( 13 | 'foo = [fn(x) for x in items]', 14 | id='assignment to single variable', 15 | ), 16 | pytest.param( 17 | 'x, = [await foo for foo in bar]', 18 | id='async comprehension', 19 | ), 20 | ), 21 | ) 22 | def test_fix_typing_text_noop(s): 23 | assert _fix_plugins(s, settings=Settings()) == s 24 | 25 | 26 | @pytest.mark.parametrize( 27 | ('s', 'expected'), 28 | ( 29 | pytest.param( 30 | 'foo, bar, baz = [fn(x) for x in items]\n', 31 | 32 | 'foo, bar, baz = (fn(x) for x in items)\n', 33 | 34 | id='single-line assignment', 35 | ), 36 | pytest.param( 37 | 'foo, bar, baz = [[i for i in fn(x)] for x in items]\n', 38 | 39 | 'foo, bar, baz = ([i for i in fn(x)] for x in items)\n', 40 | 41 | id='nested list comprehension', 42 | ), 43 | pytest.param( 44 | 'foo, bar, baz = [\n' 45 | ' fn(x)\n' 46 | ' for x in items\n' 47 | ']\n', 48 | 49 | 'foo, bar, baz = (\n' 50 | ' fn(x)\n' 51 | ' for x in items\n' 52 | ')\n', 53 | 54 | id='multi-line assignment', 55 | ), 56 | ), 57 | ) 58 | def test_fix_typing_text(s, expected): 59 | ret = _fix_plugins(s, settings=Settings()) 60 | assert ret == expected 61 | -------------------------------------------------------------------------------- /tests/main_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import io 4 | import re 5 | import sys 6 | from unittest import mock 7 | 8 | import pytest 9 | 10 | from pyupgrade._main import main 11 | 12 | 13 | def test_main_trivial(): 14 | assert main(()) == 0 15 | 16 | 17 | def test_main_noop(tmpdir, capsys): 18 | with pytest.raises(SystemExit): 19 | main(('--help',)) 20 | out, err = capsys.readouterr() 21 | version_options = sorted(set(re.findall(r'--py\d+-plus', out))) 22 | 23 | s = '''\ 24 | from sys import version_info 25 | x=version_info 26 | def f(): 27 | global x, y 28 | 29 | f'hello snowman: \\N{SNOWMAN}' 30 | ''' 31 | f = tmpdir.join('f.py') 32 | f.write(s) 33 | 34 | assert main((f.strpath,)) == 0 35 | assert f.read() == s 36 | 37 | for version_option in version_options: 38 | assert main((f.strpath, version_option)) == 0 39 | assert f.read() == s 40 | 41 | 42 | def test_main_changes_a_file(tmpdir, capsys): 43 | f = tmpdir.join('f.py') 44 | f.write('x = set((1, 2, 3))\n') 45 | assert main((f.strpath,)) == 1 46 | out, err = capsys.readouterr() 47 | assert err == f'Rewriting {f.strpath}\n' 48 | assert f.read() == 'x = {1, 2, 3}\n' 49 | 50 | 51 | def test_main_keeps_line_endings(tmpdir, capsys): 52 | f = tmpdir.join('f.py') 53 | f.write_binary(b'x = set((1, 2, 3))\r\n') 54 | assert main((f.strpath,)) == 1 55 | assert f.read_binary() == b'x = {1, 2, 3}\r\n' 56 | 57 | 58 | def test_main_syntax_error(tmpdir): 59 | f = tmpdir.join('f.py') 60 | f.write('from __future__ import print_function\nprint 1\n') 61 | assert main((f.strpath,)) == 0 62 | 63 | 64 | def test_main_non_utf8_bytes(tmpdir, capsys): 65 | f = tmpdir.join('f.py') 66 | f.write_binary('# -*- coding: cp1252 -*-\nx = €\n'.encode('cp1252')) 67 | assert main((f.strpath,)) == 1 68 | out, _ = capsys.readouterr() 69 | assert out == f'{f.strpath} is non-utf-8 (not supported)\n' 70 | 71 | 72 | def test_keep_percent_format(tmpdir): 73 | f = tmpdir.join('f.py') 74 | f.write('"%s" % (1,)') 75 | assert main((f.strpath, '--keep-percent-format')) == 0 76 | assert f.read() == '"%s" % (1,)' 77 | assert main((f.strpath,)) == 1 78 | assert f.read() == '"{}".format(1)' 79 | 80 | 81 | def test_keep_mock(tmpdir): 82 | f = tmpdir.join('f.py') 83 | f.write('from mock import patch\n') 84 | assert main((f.strpath, '--keep-mock')) == 0 85 | assert f.read() == 'from mock import patch\n' 86 | assert main((f.strpath,)) == 1 87 | assert f.read() == 'from unittest.mock import patch\n' 88 | 89 | 90 | def test_py3_plus_argument_unicode_literals(tmpdir): 91 | f = tmpdir.join('f.py') 92 | f.write('u""') 93 | assert main((f.strpath,)) == 1 94 | assert f.read() == '""' 95 | 96 | 97 | def test_py3_plus_super(tmpdir): 98 | f = tmpdir.join('f.py') 99 | f.write( 100 | 'class C(Base):\n' 101 | ' def f(self):\n' 102 | ' super(C, self).f()\n', 103 | ) 104 | assert main((f.strpath,)) == 1 105 | assert f.read() == ( 106 | 'class C(Base):\n' 107 | ' def f(self):\n' 108 | ' super().f()\n' 109 | ) 110 | 111 | 112 | def test_py3_plus_new_style_classes(tmpdir): 113 | f = tmpdir.join('f.py') 114 | f.write('class C(object): pass\n') 115 | assert main((f.strpath,)) == 1 116 | assert f.read() == 'class C: pass\n' 117 | 118 | 119 | def test_py3_plus_oserror(tmpdir): 120 | f = tmpdir.join('f.py') 121 | f.write('raise EnvironmentError(1, 2)\n') 122 | assert main((f.strpath,)) == 1 123 | assert f.read() == 'raise OSError(1, 2)\n' 124 | 125 | 126 | def test_py36_plus_fstrings(tmpdir): 127 | f = tmpdir.join('f.py') 128 | f.write('"{} {}".format(hello, world)') 129 | assert main((f.strpath,)) == 0 130 | assert f.read() == '"{} {}".format(hello, world)' 131 | assert main((f.strpath, '--py36-plus')) == 1 132 | assert f.read() == 'f"{hello} {world}"' 133 | 134 | 135 | def test_py37_plus_removes_annotations(tmpdir): 136 | f = tmpdir.join('f.py') 137 | f.write('from __future__ import generator_stop\nx = 1\n') 138 | assert main((f.strpath,)) == 0 139 | assert main((f.strpath, '--py36-plus')) == 0 140 | assert main((f.strpath, '--py37-plus')) == 1 141 | assert f.read() == 'x = 1\n' 142 | 143 | 144 | def test_py38_plus_removes_no_arg_decorators(tmpdir): 145 | f = tmpdir.join('f.py') 146 | f.write( 147 | 'import functools\n\n' 148 | '@functools.lru_cache()\n' 149 | 'def expensive():\n' 150 | ' ...', 151 | ) 152 | assert main((f.strpath,)) == 0 153 | assert main((f.strpath, '--py36-plus')) == 0 154 | assert main((f.strpath, '--py37-plus')) == 0 155 | assert main((f.strpath, '--py38-plus')) == 1 156 | assert f.read() == ( 157 | 'import functools\n\n' 158 | '@functools.lru_cache\n' 159 | 'def expensive():\n' 160 | ' ...' 161 | ) 162 | 163 | 164 | def test_noop_token_error(tmpdir): 165 | f = tmpdir.join('f.py') 166 | f.write( 167 | # force some rewrites (ast is ok https://bugs.python.org/issue2180) 168 | 'set(())\n' 169 | '"%s" % (1,)\n' 170 | 'six.b("foo")\n' 171 | '"{}".format(a)\n' 172 | # token error 173 | 'x = \\\n' 174 | '5\\\n', 175 | ) 176 | assert main((f.strpath, '--py36-plus')) == 0 177 | 178 | 179 | def test_main_exit_zero_even_if_changed(tmpdir): 180 | f = tmpdir.join('t.py') 181 | f.write('set((1, 2))\n') 182 | assert not main((str(f), '--exit-zero-even-if-changed')) 183 | assert f.read() == '{1, 2}\n' 184 | assert not main((str(f), '--exit-zero-even-if-changed')) 185 | 186 | 187 | def test_main_stdin_no_changes(capsys): 188 | stdin = io.TextIOWrapper(io.BytesIO(b'{1, 2}\n'), 'UTF-8') 189 | with mock.patch.object(sys, 'stdin', stdin): 190 | assert main(('-',)) == 0 191 | out, err = capsys.readouterr() 192 | assert out == '{1, 2}\n' 193 | 194 | 195 | def test_main_stdin_with_changes(capsys): 196 | stdin = io.TextIOWrapper(io.BytesIO(b'set((1, 2))\n'), 'UTF-8') 197 | with mock.patch.object(sys, 'stdin', stdin): 198 | assert main(('-',)) == 1 199 | out, err = capsys.readouterr() 200 | assert out == '{1, 2}\n' 201 | -------------------------------------------------------------------------------- /tests/string_helpers_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from pyupgrade._string_helpers import parse_format 6 | from pyupgrade._string_helpers import unparse_parsed_string 7 | 8 | 9 | @pytest.mark.parametrize( 10 | 's', 11 | ( 12 | '', 'foo', '{}', '{0}', '{named}', '{!r}', '{:>5}', '{{', '}}', 13 | '{0!s:15}', 14 | ), 15 | ) 16 | def test_roundtrip_text(s): 17 | assert unparse_parsed_string(parse_format(s)) == s 18 | 19 | 20 | def test_parse_format_starts_with_named(): 21 | # technically not possible since our string always starts with quotes 22 | assert parse_format(r'\N{snowman} hi {0} hello') == [ 23 | (r'\N{snowman} hi ', '0', '', None), 24 | (' hello', None, None, None), 25 | ] 26 | 27 | 28 | @pytest.mark.parametrize( 29 | ('s', 'expected'), 30 | ( 31 | ('{:}', '{}'), 32 | ('{0:}', '{0}'), 33 | ('{0!r:}', '{0!r}'), 34 | ), 35 | ) 36 | def test_intentionally_not_round_trip(s, expected): 37 | # Our unparse simplifies empty parts, whereas stdlib allows them 38 | ret = unparse_parsed_string(parse_format(s)) 39 | assert ret == expected 40 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py,pypy3,pre-commit 3 | 4 | [testenv] 5 | deps = -rrequirements-dev.txt 6 | commands = 7 | coverage erase 8 | coverage run -m pytest {posargs:tests} 9 | coverage report 10 | 11 | [testenv:pre-commit] 12 | skip_install = true 13 | deps = pre-commit 14 | commands = pre-commit run --all-files --show-diff-on-failure 15 | 16 | [pep8] 17 | ignore = E265,E501,W504 18 | --------------------------------------------------------------------------------