From aa9d9656d2bb6bf075406c676f33844cd3555a17 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 6 Oct 2022 15:14:43 -0700 Subject: [PATCH 1/4] rewrite set((\n)) there is a comment in the code about this causing a SyntaxError but as far as I can tell that is incorrect --- pyupgrade/_plugins/set_literals.py | 3 --- tests/features/set_literals_test.py | 4 +--- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/pyupgrade/_plugins/set_literals.py b/pyupgrade/_plugins/set_literals.py index ed56776c..25630199 100644 --- a/pyupgrade/_plugins/set_literals.py +++ b/pyupgrade/_plugins/set_literals.py @@ -32,9 +32,6 @@ def _fix_set_empty_literal(i: int, tokens: list[Token]) -> None: brace_stack.pop() elif token in BRACES: brace_stack.append(token) - elif '\n' in token: - # Contains a newline, could cause a SyntaxError, bail - return j += 1 # Remove the inner tokens diff --git a/tests/features/set_literals_test.py b/tests/features/set_literals_test.py index 41c9d38d..ea3bb5c9 100644 --- a/tests/features/set_literals_test.py +++ b/tests/features/set_literals_test.py @@ -11,9 +11,6 @@ ( # Don't touch empty set literals 'set()', - # Don't touch set(empty literal) with newlines in them (may create - # syntax errors) - 'set((\n))', # Don't touch weird looking function calls -- use autopep8 or such # first 'set (())', 'set ((1, 2))', @@ -92,6 +89,7 @@ def test_fix_sets_noop(s): ' 99, 100,\n' '}\n', ), + pytest.param('set((\n))', 'set()', id='empty literal with newline'), ), ) def test_sets(s, expected): From 981bbc6269d2a348f21921427ff234add2d24ff3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 6 Oct 2022 15:59:31 -0700 Subject: [PATCH 2/4] handle redundant t mode --- README.md | 2 ++ pyupgrade/_plugins/open_mode.py | 41 ++++++++++++++++++++------------ tests/features/open_mode_test.py | 19 +++++++++++++-- 3 files changed, 45 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 9ea2cd56..b0aea17a 100644 --- a/README.md +++ b/README.md @@ -487,6 +487,8 @@ Note that `if` blocks without an `else` will not be rewritten as it could introd +open("foo") -open("f", "r", encoding="UTF-8") +open("f", encoding="UTF-8") +-open("f", "wt") ++open("f", "w") ``` diff --git a/pyupgrade/_plugins/open_mode.py b/pyupgrade/_plugins/open_mode.py index 38b3e722..f786ba9b 100644 --- a/pyupgrade/_plugins/open_mode.py +++ b/pyupgrade/_plugins/open_mode.py @@ -2,6 +2,7 @@ import ast import functools +import itertools from typing import Iterable from typing import NamedTuple @@ -18,10 +19,20 @@ from pyupgrade._token_helpers import find_open_paren from pyupgrade._token_helpers import parse_call_args -U_MODE_REMOVE = frozenset(('U', 'Ur', 'rU', 'r', 'rt', 'tr')) -U_MODE_REPLACE_R = frozenset(('Ub', 'bU')) -U_MODE_REMOVE_U = frozenset(('rUb', 'Urb', 'rbU', 'Ubr', 'bUr', 'brU')) -U_MODE_REPLACE = U_MODE_REPLACE_R | U_MODE_REMOVE_U + +def _plus(args: tuple[str, ...]) -> tuple[str, ...]: + return args + tuple(f'{arg}+' for arg in args) + + +def _permute(*args: str) -> tuple[str, ...]: + return tuple(''.join(p) for s in args for p in itertools.permutations(s)) + + +MODE_REMOVE = frozenset(_permute('U', 'r', 'rU', 'rt')) +MODE_REPLACE_R = frozenset(_permute('Ub')) +MODE_REMOVE_T = frozenset(_plus(_permute('at', 'rt', 'wt', 'xt'))) +MODE_REMOVE_U = frozenset(_permute('rUb')) +MODE_REPLACE = MODE_REPLACE_R | MODE_REMOVE_T | MODE_REMOVE_U class FunctionArg(NamedTuple): @@ -35,12 +46,15 @@ def _fix_open_mode(i: int, tokens: list[Token], *, arg_idx: int) -> None: mode = tokens_to_src(tokens[slice(*func_args[arg_idx])]) mode_stripped = mode.split('=')[-1] mode_stripped = ast.literal_eval(mode_stripped.strip()) - if mode_stripped in U_MODE_REMOVE: + if mode_stripped in MODE_REMOVE: delete_argument(arg_idx, tokens, func_args) - elif mode_stripped in U_MODE_REPLACE_R: + elif mode_stripped in MODE_REPLACE_R: new_mode = mode.replace('U', 'r') tokens[slice(*func_args[arg_idx])] = [Token('SRC', new_mode)] - elif mode_stripped in U_MODE_REMOVE_U: + elif mode_stripped in MODE_REMOVE_T: + new_mode = mode.replace('t', '') + tokens[slice(*func_args[arg_idx])] = [Token('SRC', new_mode)] + elif mode_stripped in MODE_REMOVE_U: new_mode = mode.replace('U', '') tokens[slice(*func_args[arg_idx])] = [Token('SRC', new_mode)] else: @@ -69,13 +83,10 @@ def visit_Call( ): if len(node.args) >= 2 and isinstance(node.args[1], ast.Str): if ( - node.args[1].s in U_MODE_REPLACE or - (len(node.args) == 2 and node.args[1].s in U_MODE_REMOVE) + node.args[1].s in MODE_REPLACE or + (len(node.args) == 2 and node.args[1].s in MODE_REMOVE) ): - func = functools.partial( - _fix_open_mode, - arg_idx=1, - ) + func = functools.partial(_fix_open_mode, arg_idx=1) yield ast_to_offset(node), func elif node.keywords and (len(node.keywords) + len(node.args) > 1): mode = next( @@ -90,8 +101,8 @@ def visit_Call( mode is not None and isinstance(mode.value, ast.Str) and ( - mode.value.s in U_MODE_REMOVE or - mode.value.s in U_MODE_REPLACE + mode.value.s in MODE_REMOVE or + mode.value.s in MODE_REPLACE ) ): func = functools.partial( diff --git a/tests/features/open_mode_test.py b/tests/features/open_mode_test.py index c8064bfd..8e19197d 100644 --- a/tests/features/open_mode_test.py +++ b/tests/features/open_mode_test.py @@ -4,6 +4,18 @@ from pyupgrade._data import Settings from pyupgrade._main import _fix_plugins +from pyupgrade._plugins.open_mode import _permute +from pyupgrade._plugins.open_mode import _plus + + +def test_plus(): + assert _plus(('a',)) == ('a', 'a+') + assert _plus(('a', 'b')) == ('a', 'b', 'a+', 'b+') + + +def test_permute(): + assert _permute('ab') == ('ab', 'ba') + assert _permute('abc') == ('abc', 'acb', 'bac', 'bca', 'cab', 'cba') @pytest.mark.parametrize( @@ -18,8 +30,6 @@ 'open("foo", qux="r")', 'open("foo", 3)', 'open(mode="r")', - # TODO: could maybe be rewritten to remove t? - 'open("foo", "wt")', # don't remove this, they meant to use `encoding=` 'open("foo", "r", "utf-8")', ), @@ -70,6 +80,11 @@ def test_fix_open_mode_noop(s): 'open("foo")', id='io.open also rewrites modes in a single pass', ), + ('open("foo", "wt")', 'open("foo", "w")'), + ('open("foo", "xt")', 'open("foo", "x")'), + ('open("foo", "at")', 'open("foo", "a")'), + ('open("foo", "wt+")', 'open("foo", "w+")'), + ('open("foo", "rt+")', 'open("foo", "r+")'), ), ) def test_fix_open_mode(s, expected): From 2d46dc588222569b5891cf0e5333ae025c9b86a0 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 10 Oct 2022 17:50:22 -0400 Subject: [PATCH 3/4] sync import rewrites from latest reorder-python-imports --- pyupgrade/_plugins/imports.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyupgrade/_plugins/imports.py b/pyupgrade/_plugins/imports.py index 5e9cf9a9..fa2d3a7b 100644 --- a/pyupgrade/_plugins/imports.py +++ b/pyupgrade/_plugins/imports.py @@ -21,7 +21,7 @@ from pyupgrade._token_helpers import indented_amount # GENERATED VIA generate-imports -# Using reorder-python-imports==3.8.3 +# Using reorder-python-imports==3.8.4 REMOVALS = { (3,): { '__future__': { @@ -156,7 +156,6 @@ (3, 10): { ('typing', 'Callable'): 'collections.abc', ('typing_extensions', 'Concatenate'): 'typing', - ('typing_extensions', 'ParamSpec'): 'typing', ('typing_extensions', 'ParamSpecArgs'): 'typing', ('typing_extensions', 'ParamSpecKwargs'): 'typing', ('typing_extensions', 'TypeAlias'): 'typing', From d50ce21fc45d014790d4ecf71d1b1d7ccc450526 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 10 Oct 2022 18:03:27 -0400 Subject: [PATCH 4/4] v3.1.0 --- .pre-commit-config.yaml | 2 +- README.md | 2 +- setup.cfg | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 21114ed7..6637bc89 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v3.0.0 + rev: v3.1.0 hooks: - id: pyupgrade args: [--py37-plus] diff --git a/README.md b/README.md index b0aea17a..af881ea5 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Sample `.pre-commit-config.yaml`: ```yaml - repo: https://github.com/asottile/pyupgrade - rev: v3.0.0 + rev: v3.1.0 hooks: - id: pyupgrade ``` diff --git a/setup.cfg b/setup.cfg index bc6ec002..e11dd234 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pyupgrade -version = 3.0.0 +version = 3.1.0 description = A tool to automatically upgrade syntax for newer versions. long_description = file: README.md long_description_content_type = text/markdown