From a9a451c20a47133825dfe682ec05dce2bffd6f8c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 2 Aug 2021 16:05:51 -0700 Subject: [PATCH 01/54] correctly compute indent for typeddict after dedent --- pyupgrade/_main.py | 2 ++ tests/features/typing_typed_dict_test.py | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/pyupgrade/_main.py b/pyupgrade/_main.py index f27807a9..291fc47a 100644 --- a/pyupgrade/_main.py +++ b/pyupgrade/_main.py @@ -746,6 +746,8 @@ def _typed_class_replacement( call: ast.Call, types: Dict[str, ast.expr], ) -> Tuple[int, str]: + while i > 0 and tokens[i - 1].name == 'DEDENT': + i -= 1 if i > 0 and tokens[i - 1].name in {'INDENT', UNIMPORTANT_WS}: indent = f'{tokens[i - 1].src}{" " * 4}' else: diff --git a/tests/features/typing_typed_dict_test.py b/tests/features/typing_typed_dict_test.py index 0ead5842..b3a4bf1c 100644 --- a/tests/features/typing_typed_dict_test.py +++ b/tests/features/typing_typed_dict_test.py @@ -134,6 +134,22 @@ def test_typing_typed_dict_noop(s): id='index unparse error', ), + pytest.param( + 'import typing\n' + 'if True:\n' + ' if False:\n' + ' pass\n' + ' D = typing.TypedDict("D", a=int)\n', + + 'import typing\n' + 'if True:\n' + ' if False:\n' + ' pass\n' + ' class D(typing.TypedDict):\n' + ' a: int\n', + + id='right after a dedent', + ), ), ) def test_typing_typed_dict(s, expected): From a638b1c3410109247dd0398356789670f1bc3c82 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 2 Aug 2021 16:15:44 -0700 Subject: [PATCH 02/54] v2.23.2 --- .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 3c7c2c6b..9d4d2fa0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v2.23.1 + rev: v2.23.2 hooks: - id: pyupgrade args: [--py36-plus] diff --git a/README.md b/README.md index 93f5fa51..ac399c95 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Sample `.pre-commit-config.yaml`: ```yaml - repo: https://github.com/asottile/pyupgrade - rev: v2.23.1 + rev: v2.23.2 hooks: - id: pyupgrade ``` diff --git a/setup.cfg b/setup.cfg index 00ae4265..9d4414f3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pyupgrade -version = 2.23.1 +version = 2.23.2 description = A tool to automatically upgrade syntax for newer versions. long_description = file: README.md long_description_content_type = text/markdown From 1ba791ec365b929c9484a1f45d5d094e95eec51a Mon Sep 17 00:00:00 2001 From: MarcoGorelli Date: Tue, 3 Aug 2021 18:47:16 +0100 Subject: [PATCH 03/54] add extra test for pep584 --- tests/features/pep584_test.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/features/pep584_test.py b/tests/features/pep584_test.py index 357b915a..aa5cd0cf 100644 --- a/tests/features/pep584_test.py +++ b/tests/features/pep584_test.py @@ -104,6 +104,19 @@ def test_fix_pep584_noop(s, version): id='Dict comprehension within merge of dicts', ), + pytest.param( + 'x = {\n' + ' **{a: b for a, b in zip(range(3), range(3))},\n' + ' **b,\n' + '}\n', + + 'x = (\n' + ' {a: b for a, b in zip(range(3), range(3))} |\n' + ' b\n' + ')\n', + + id='Dict with comma inside it', + ), ), ) def test_fix_pep584(s, expected): From 6eaa2a75e7af59e960633c50a7d505ac2e15ef2a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 4 Aug 2021 10:43:10 -0700 Subject: [PATCH 04/54] cannot unpack async generators --- pyupgrade/_ast_helpers.py | 7 +++++++ pyupgrade/_plugins/generator_expressions_pep289.py | 8 ++------ pyupgrade/_plugins/unpack_list_comprehension.py | 4 +++- tests/features/unpack_list_comprehension_test.py | 5 +++++ 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/pyupgrade/_ast_helpers.py b/pyupgrade/_ast_helpers.py index 6a556c5a..eb4ba066 100644 --- a/pyupgrade/_ast_helpers.py +++ b/pyupgrade/_ast_helpers.py @@ -50,3 +50,10 @@ def contains_await(node: ast.AST) -> bool: return True else: return False + + +def is_async_listcomp(node: ast.ListComp) -> bool: + return ( + any(gen.is_async for gen in node.generators) or + contains_await(node) + ) diff --git a/pyupgrade/_plugins/generator_expressions_pep289.py b/pyupgrade/_plugins/generator_expressions_pep289.py index f90a5913..16eb2629 100644 --- a/pyupgrade/_plugins/generator_expressions_pep289.py +++ b/pyupgrade/_plugins/generator_expressions_pep289.py @@ -8,7 +8,7 @@ from tokenize_rt import Token from pyupgrade._ast_helpers import ast_to_offset -from pyupgrade._ast_helpers import contains_await +from pyupgrade._ast_helpers import is_async_listcomp from pyupgrade._data import register from pyupgrade._data import State from pyupgrade._data import TokenFunc @@ -53,11 +53,7 @@ def visit_Call( node.func.id in ALLOWED_FUNCS and node.args and isinstance(node.args[0], ast.ListComp) and - not any( - generator.is_async - for generator in node.args[0].generators - ) and - not contains_await(node.args[0]) + not is_async_listcomp(node.args[0]) ): if len(node.args) == 1 and not node.keywords: yield ast_to_offset(node.args[0]), _delete_list_comp_brackets diff --git a/pyupgrade/_plugins/unpack_list_comprehension.py b/pyupgrade/_plugins/unpack_list_comprehension.py index b372813a..f25d1625 100644 --- a/pyupgrade/_plugins/unpack_list_comprehension.py +++ b/pyupgrade/_plugins/unpack_list_comprehension.py @@ -7,6 +7,7 @@ from tokenize_rt import Token from pyupgrade._ast_helpers import ast_to_offset +from pyupgrade._ast_helpers import is_async_listcomp from pyupgrade._data import register from pyupgrade._data import State from pyupgrade._data import TokenFunc @@ -31,6 +32,7 @@ def visit_Assign( state.settings.min_version >= (3,) and len(node.targets) == 1 and isinstance(node.targets[0], ast.Tuple) and - isinstance(node.value, ast.ListComp) + isinstance(node.value, ast.ListComp) and + not is_async_listcomp(node.value) ): yield ast_to_offset(node.value), _replace_list_comprehension diff --git a/tests/features/unpack_list_comprehension_test.py b/tests/features/unpack_list_comprehension_test.py index c264d35c..75797e77 100644 --- a/tests/features/unpack_list_comprehension_test.py +++ b/tests/features/unpack_list_comprehension_test.py @@ -17,6 +17,11 @@ (3,), id='assignment to single variable', ), + pytest.param( + 'x, = [await foo for foo in bar]', + (3,), + id='async comprehension', + ), ), ) def test_fix_typing_text_noop(s, version): From 548d21cbf404a14145f3833978f96f4f36de6469 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 4 Aug 2021 10:45:43 -0700 Subject: [PATCH 05/54] can't splat async generators --- pyupgrade/_plugins/unpacking_argument_list_comprehensions.py | 4 +++- .../features/unpacking_argument_list_comprehensions_test.py | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/pyupgrade/_plugins/unpacking_argument_list_comprehensions.py b/pyupgrade/_plugins/unpacking_argument_list_comprehensions.py index 7b94032f..87bafd96 100644 --- a/pyupgrade/_plugins/unpacking_argument_list_comprehensions.py +++ b/pyupgrade/_plugins/unpacking_argument_list_comprehensions.py @@ -5,6 +5,7 @@ from tokenize_rt import Offset from pyupgrade._ast_helpers import ast_to_offset +from pyupgrade._ast_helpers import is_async_listcomp from pyupgrade._data import register from pyupgrade._data import State from pyupgrade._data import TokenFunc @@ -19,6 +20,7 @@ def visit_Starred( ) -> Iterable[Tuple[Offset, TokenFunc]]: if ( state.settings.min_version >= (3,) and - isinstance(node.value, ast.ListComp) + isinstance(node.value, ast.ListComp) and + not is_async_listcomp(node.value) ): yield ast_to_offset(node.value), replace_list_comp_brackets diff --git a/tests/features/unpacking_argument_list_comprehensions_test.py b/tests/features/unpacking_argument_list_comprehensions_test.py index ce4487c9..7e41929d 100644 --- a/tests/features/unpacking_argument_list_comprehensions_test.py +++ b/tests/features/unpacking_argument_list_comprehensions_test.py @@ -32,6 +32,11 @@ (3,), id='Starred, no list comp', ), + pytest.param( + 'foo(*[x async for x in bar])', + (3,), + id='async listcomp', + ), ), ) def test_fix_unpack_argument_list_comp_noop(s, version): From 53c94718aae4074edde7099996c81ab7cc081ce3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 4 Aug 2021 10:50:42 -0700 Subject: [PATCH 06/54] v2.23.3 --- .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 9d4d2fa0..5b63695c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v2.23.2 + rev: v2.23.3 hooks: - id: pyupgrade args: [--py36-plus] diff --git a/README.md b/README.md index ac399c95..cf8952d3 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Sample `.pre-commit-config.yaml`: ```yaml - repo: https://github.com/asottile/pyupgrade - rev: v2.23.2 + rev: v2.23.3 hooks: - id: pyupgrade ``` diff --git a/setup.cfg b/setup.cfg index 9d4414f3..0768cb06 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pyupgrade -version = 2.23.2 +version = 2.23.3 description = A tool to automatically upgrade syntax for newer versions. long_description = file: README.md long_description_content_type = text/markdown From 092642af377bd17cd08d6a126e70d061c0764b36 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Tue, 17 Aug 2021 09:40:23 +0100 Subject: [PATCH 07/54] Rewrite unittest deprecated aliases Fixes #519. --- README.md | 20 +++++++- pyupgrade/_plugins/unittest_aliases.py | 60 +++++++++++++++++++++++ tests/features/unittest_aliases_test.py | 63 +++++++++++++++++++++++++ 3 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 pyupgrade/_plugins/unittest_aliases.py create mode 100644 tests/features/unittest_aliases_test.py diff --git a/README.md b/README.md index cf8952d3..6154d265 100644 --- a/README.md +++ b/README.md @@ -165,6 +165,25 @@ print(("foo")) # print("foo") [python-modernize/python-modernize#178]: https://github.com/python-modernize/python-modernize/issues/178 +### unittest deprecated aliases + +Rewrites [deprecated unittest method aliases](https://docs.python.org/3/library/unittest.html#deprecated-aliases) to their non-deprecated forms. + +Availability: +- More deprecated aliases are rewritten with `--py3-plus` + +```diff + from unittest import TestCase + + + class MyTests(TestCase): + def test_something(self): +- self.failUnlessEqual(1, 1) ++ self.assertEqual(1, 1) +- self.assertEquals(1, 1) ++ self.assertEqual(1, 1) +``` + ### `super()` calls Availability: @@ -482,7 +501,6 @@ Availability: +float ``` - ### `typing.NamedTuple` / `typing.TypedDict` py36+ syntax Availability: diff --git a/pyupgrade/_plugins/unittest_aliases.py b/pyupgrade/_plugins/unittest_aliases.py new file mode 100644 index 00000000..3030ee8a --- /dev/null +++ b/pyupgrade/_plugins/unittest_aliases.py @@ -0,0 +1,60 @@ +import ast +import functools +from typing import Iterable +from typing import Tuple + +from tokenize_rt import Offset + +from pyupgrade._ast_helpers import ast_to_offset +from pyupgrade._data import register +from pyupgrade._data import State +from pyupgrade._data import TokenFunc +from pyupgrade._token_helpers import replace_name + + +METHOD_MAPPING_PY27 = { + 'assertEquals': 'assertEqual', + 'failUnlessEqual': 'assertEqual', + 'failIfEqual': 'assertNotEqual', + 'failUnless': 'assertTrue', + 'assert_': 'assertTrue', + 'failIf': 'assertFalse', + 'failUnlessRaises': 'assertRaises', + 'failUnlessAlmostEqual': 'assertAlmostEqual', + 'failIfAlmostEqual': 'assertNotAlmostEqual', +} + +METHOD_MAPPING_PY35_PLUS = { + **METHOD_MAPPING_PY27, + 'assertNotEquals': 'assertNotEqual', + 'assertAlmostEquals': 'assertAlmostEqual', + 'assertNotAlmostEquals': 'assertNotAlmostEqual', + 'assertRegexpMatches': 'assertRegex', + 'assertNotRegexpMatches': 'assertNotRegex', + 'assertRaisesRegexp': 'assertRaisesRegex', +} + + +@register(ast.Call) +def visit_Call( + state: State, + node: ast.Call, + parent: ast.AST, +) -> Iterable[Tuple[Offset, TokenFunc]]: + if state.settings.min_version >= (3,): + method_mapping = METHOD_MAPPING_PY35_PLUS + else: + method_mapping = METHOD_MAPPING_PY27 + + if ( + isinstance(node.func, ast.Attribute) and + isinstance(node.func.value, ast.Name) and + node.func.value.id == 'self' and + node.func.attr in method_mapping + ): + func = functools.partial( + replace_name, + name=node.func.attr, + new=f'self.{method_mapping[node.func.attr]}', + ) + yield ast_to_offset(node.func), func diff --git a/tests/features/unittest_aliases_test.py b/tests/features/unittest_aliases_test.py new file mode 100644 index 00000000..1dc6dfd0 --- /dev/null +++ b/tests/features/unittest_aliases_test.py @@ -0,0 +1,63 @@ +import pytest + +from pyupgrade._data import Settings +from pyupgrade._main import _fix_plugins + + +@pytest.mark.parametrize( + ('s',), + ( + pytest.param( + 'class ExampleTests:\n' + ' def test_something(self):\n' + ' self.assertEqual(1, 1)\n', + id='not a deprecated alias', + ), + pytest.param( + 'class ExampleTests:\n' + ' def test_something(self):\n' + ' self.assertNotEquals(1, 2)\n', + id='not python 3+', + ), + ), +) +def test_fix_unittest_aliases_noop(s): + assert _fix_plugins(s, settings=Settings(min_version=(2, 7))) == s + + +@pytest.mark.parametrize( + ('s', 'expected'), + ( + ( + 'class ExampleTests:\n' + ' def test_something(self):\n' + ' self.assertEquals(1, 1)\n', + + 'class ExampleTests:\n' + ' def test_something(self):\n' + ' self.assertEqual(1, 1)\n', + ), + ), +) +def test_fix_unittest_aliases_py27(s, expected): + ret = _fix_plugins(s, settings=Settings(min_version=(2, 7))) + assert ret == expected + + +@pytest.mark.parametrize( + ('s', 'expected'), + ( + ( + 'class ExampleTests:\n' + ' def test_something(self):\n' + ' self.assertNotEquals(1, 2)\n', + + 'class ExampleTests:\n' + ' def test_something(self):\n' + ' self.assertNotEqual(1, 2)\n', + ), + ), +) +def test_fix_unittest_aliases_py3(s, expected): + ret = _fix_plugins(s, settings=Settings(min_version=(3,))) + assert ret == expected From aaad686b5f7851dd1b269f87cb5832ed132f4631 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 19 Aug 2021 18:33:53 -0400 Subject: [PATCH 08/54] v2.24.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 5b63695c..5476c83b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v2.23.3 + rev: v2.24.0 hooks: - id: pyupgrade args: [--py36-plus] diff --git a/README.md b/README.md index 6154d265..1e5fa1f9 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Sample `.pre-commit-config.yaml`: ```yaml - repo: https://github.com/asottile/pyupgrade - rev: v2.23.3 + rev: v2.24.0 hooks: - id: pyupgrade ``` diff --git a/setup.cfg b/setup.cfg index 0768cb06..5ff96c2b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pyupgrade -version = 2.23.3 +version = 2.24.0 description = A tool to automatically upgrade syntax for newer versions. long_description = file: README.md long_description_content_type = text/markdown From 1cf9acaa31b362bf87765177a9f4c73aca91b02f Mon Sep 17 00:00:00 2001 From: Marco Gorelli Date: Sat, 28 Aug 2021 09:03:55 +0100 Subject: [PATCH 09/54] revert pep584 rewrite --- README.md | 13 ---- pyupgrade/_plugins/pep584.py | 61 ----------------- tests/features/pep584_test.py | 123 ---------------------------------- 3 files changed, 197 deletions(-) delete mode 100644 pyupgrade/_plugins/pep584.py delete mode 100644 tests/features/pep584_test.py diff --git a/README.md b/README.md index 1e5fa1f9..a1426538 100644 --- a/README.md +++ b/README.md @@ -598,19 +598,6 @@ Availability: ``` -### merge dicts using union operator (pep 584) - -Availability: -- `--py39-plus` is passed on the commandline. - -```diff - x = {"a": 1} - y = {"b": 2} --z = {**x, **y} -+z = x | y -``` - - ### pep 585 typing rewrites Availability: diff --git a/pyupgrade/_plugins/pep584.py b/pyupgrade/_plugins/pep584.py deleted file mode 100644 index db2a16a6..00000000 --- a/pyupgrade/_plugins/pep584.py +++ /dev/null @@ -1,61 +0,0 @@ -import ast -from typing import Iterable -from typing import Tuple - -from tokenize_rt import List -from tokenize_rt import NON_CODING_TOKENS -from tokenize_rt import Offset -from tokenize_rt import Token - -from pyupgrade._ast_helpers import ast_to_offset -from pyupgrade._data import register -from pyupgrade._data import State -from pyupgrade._data import TokenFunc -from pyupgrade._token_helpers import find_closing_bracket - - -def _replace_dict_brackets(i: int, tokens: List[Token]) -> None: - closing = find_closing_bracket(tokens, i) - j = closing - 1 - while tokens[j].name in NON_CODING_TOKENS and j > i: - j -= 1 - if tokens[j].name == 'OP' and tokens[j].src == ',': - tokens[j] = Token('PLACEHOLDER', '') - - if tokens[i].line == tokens[closing].line: - tokens[i] = Token('PLACEHOLDER', '') - tokens[closing] = Token('PLACEHOLDER', '') - else: - tokens[i] = Token('CODE', '(') - tokens[closing] = Token('CODE', ')') - - -def _remove_double_star(i: int, tokens: List[Token]) -> None: - j = i - while not (tokens[j].name == 'OP' and tokens[j].src == '**'): - j -= 1 - tokens[j] = Token('PLACEHOLDER', '') - - -def _replace_comma_with_pipe(i: int, tokens: List[Token]) -> None: - j = i - 1 - while not (tokens[j].name == 'OP' and tokens[j].src == ','): - j -= 1 - tokens[j] = Token('CODE', ' |') - - -@register(ast.Dict) -def visit_Dict( - state: State, - node: ast.Dict, - parent: ast.AST, -) -> Iterable[Tuple[Offset, TokenFunc]]: - if state.settings.min_version < (3, 9): - return - - if all(key is None for key in node.keys) and len(node.values) > 1: - yield ast_to_offset(node), _replace_dict_brackets - for idx, arg in enumerate(node.values): - yield ast_to_offset(arg), _remove_double_star - if idx > 0: - yield ast_to_offset(arg), _replace_comma_with_pipe diff --git a/tests/features/pep584_test.py b/tests/features/pep584_test.py deleted file mode 100644 index aa5cd0cf..00000000 --- a/tests/features/pep584_test.py +++ /dev/null @@ -1,123 +0,0 @@ -import pytest - -from pyupgrade._data import Settings -from pyupgrade._main import _fix_plugins - - -@pytest.mark.parametrize( - ('s', 'version'), - ( - pytest.param( - '{**a, **b}\n', - (3, 8), - id='<3.9', - ), - pytest.param( - '{"a": 0}\n', - (3, 9), - id='Dict without merge', - ), - pytest.param( - 'x = {**a}\n', - (3, 9), - id='Merge of only one dict', - ), - ), -) -def test_fix_pep584_noop(s, version): - assert _fix_plugins(s, settings=Settings(min_version=version)) == s - - -@pytest.mark.parametrize( - ('s', 'expected'), - ( - pytest.param( - 'x = {**a, **b}\n', - - 'x = a | b\n', - - id='Simple dict rewrite', - ), - pytest.param( - 'x = {**{**a, **b}, **c}\n', - - 'x = a | b | c\n', - - id='Nested merge of dicts', - ), - pytest.param( - 'x = {**a, **b,}\n', - - 'x = a | b\n', - - id='Trailing comma', - ), - pytest.param( - 'x = {\n' - ' **a, # foo\n' - ' **b # bar\n' - '}\n', - - 'x = (\n' - ' a | # foo\n' - ' b # bar\n' - ')\n', - - id='Multiple lines with comment', - ), - pytest.param( - 'x = {\n' - ' **a,\n' - ' **b\n' - '}\n', - - 'x = (\n' - ' a |\n' - ' b\n' - ')\n', - - id='Multiple lines', - ), - pytest.param( - 'x = {\n' - ' **a,\n' - ' **b,\n' - '}\n', - - 'x = (\n' - ' a |\n' - ' b\n' - ')\n', - - id='Multiple lines, trailing comma', - ), - pytest.param( - 'x = {\n' - ' **{a: a for a in range(3)},\n' - ' **b,\n' - '}\n', - - 'x = (\n' - ' {a: a for a in range(3)} |\n' - ' b\n' - ')\n', - - id='Dict comprehension within merge of dicts', - ), - pytest.param( - 'x = {\n' - ' **{a: b for a, b in zip(range(3), range(3))},\n' - ' **b,\n' - '}\n', - - 'x = (\n' - ' {a: b for a, b in zip(range(3), range(3))} |\n' - ' b\n' - ')\n', - - id='Dict with comma inside it', - ), - ), -) -def test_fix_pep584(s, expected): - assert _fix_plugins(s, settings=Settings(min_version=(3, 9))) == expected From 277aa8ff35f66fb14ada40130dadac76afaf4f1b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 28 Aug 2021 12:29:26 -0400 Subject: [PATCH 10/54] v2.25.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 5476c83b..5dc7ef6d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v2.24.0 + rev: v2.25.0 hooks: - id: pyupgrade args: [--py36-plus] diff --git a/README.md b/README.md index a1426538..22e0db35 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Sample `.pre-commit-config.yaml`: ```yaml - repo: https://github.com/asottile/pyupgrade - rev: v2.24.0 + rev: v2.25.0 hooks: - id: pyupgrade ``` diff --git a/setup.cfg b/setup.cfg index 5ff96c2b..70106215 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pyupgrade -version = 2.24.0 +version = 2.25.0 description = A tool to automatically upgrade syntax for newer versions. long_description = file: README.md long_description_content_type = text/markdown From 6b2d0d8b2293360f91082d4dcd068c36a8c568b8 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 7 Sep 2021 15:18:35 -0400 Subject: [PATCH 11/54] update timidity docs for f-strings --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 22e0db35..6a9eb15f 100644 --- a/README.md +++ b/README.md @@ -543,7 +543,7 @@ Availability: _note_: `pyupgrade` is intentionally timid and will not create an f-string if it would make the expression longer or if the substitution parameters are -anything but simple names or dotted names (as this can decrease readability). +sufficiently complicated (as this can decrease readability). ### `subprocess.run`: replace `universal_newlines` with `text` From 79311238f16854f0b661f799398bb5cb56d084d9 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 10 Sep 2021 06:11:36 -0700 Subject: [PATCH 12/54] don't rewrite %-format with width+s --- pyupgrade/_plugins/percent_format.py | 5 +++-- tests/features/percent_format_test.py | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/pyupgrade/_plugins/percent_format.py b/pyupgrade/_plugins/percent_format.py index 8dc2c762..1b0599df 100644 --- a/pyupgrade/_plugins/percent_format.py +++ b/pyupgrade/_plugins/percent_format.py @@ -128,8 +128,6 @@ def _handle_part(part: PercentFormat) -> str: if conversion == '%': return s + '%' parts = [s, '{'] - if width and conversion == 's' and not conversion_flag: - conversion_flag = '>' if conversion == 's': conversion = '' if key: @@ -276,6 +274,9 @@ def visit_BinOp( # no equivalent in format if conversion in {'a', 'r'} and nontrivial_fmt: break + # %s with None and width is not supported + if width and conversion == 's': + break # all dict substitutions must be named if isinstance(node.right, ast.Dict) and not key: break diff --git a/tests/features/percent_format_test.py b/tests/features/percent_format_test.py index 57ca3c65..0d620ce0 100644 --- a/tests/features/percent_format_test.py +++ b/tests/features/percent_format_test.py @@ -158,6 +158,7 @@ def test_simplify_conversion_flag(s, expected): '"%4%" % ()', # no equivalent in format specifier '"%.2r" % (1.25)', '"%.2a" % (1.25)', + pytest.param('"%8s" % (None,)', id='unsafe width-string conversion'), # non-string mod 'i % 3', # dict format but not keyed arguments @@ -208,8 +209,8 @@ def test_percent_format_noop_if_bug_16806(): ('"%s" % ("%s" % ("nested",),)', '"{}".format("{}".format("nested"))'), ('"%s%% percent" % (15,)', '"{}% percent".format(15)'), ('"%3f" % (15,)', '"{:3f}".format(15)'), - ('"%-5s" % ("hi",)', '"{:<5}".format("hi")'), - ('"%9s" % (5,)', '"{:>9}".format(5)'), + ('"%-5f" % (5,)', '"{:<5f}".format(5)'), + ('"%9f" % (5,)', '"{:9f}".format(5)'), ('"brace {} %s" % (1,)', '"brace {{}} {}".format(1)'), ( '"%s" % (\n' From f86dc652e1b77ad9d6632f16caebabbaa452a634 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 10 Sep 2021 08:05:30 -0700 Subject: [PATCH 13/54] v2.25.1 --- .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 5dc7ef6d..5c21a33a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v2.25.0 + rev: v2.25.1 hooks: - id: pyupgrade args: [--py36-plus] diff --git a/README.md b/README.md index 6a9eb15f..1366c75d 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Sample `.pre-commit-config.yaml`: ```yaml - repo: https://github.com/asottile/pyupgrade - rev: v2.25.0 + rev: v2.25.1 hooks: - id: pyupgrade ``` diff --git a/setup.cfg b/setup.cfg index 70106215..39ffb2ac 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pyupgrade -version = 2.25.0 +version = 2.25.1 description = A tool to automatically upgrade syntax for newer versions. long_description = file: README.md long_description_content_type = text/markdown From 31546d2cdf4a7647df46b0e9c4b771efe123ea3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Robert?= Date: Thu, 9 Sep 2021 19:24:12 +0200 Subject: [PATCH 14/54] ENH: expand versioned_branches feature to Python 3 minor version comparison (<, >, <=, >= with else) --- README.md | 41 +++++- pyupgrade/_plugins/versioned_branches.py | 36 ++++- tests/features/versioned_branches_test.py | 152 ++++++++++++++++++++++ 3 files changed, 221 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 1366c75d..45f54863 100644 --- a/README.md +++ b/README.md @@ -294,21 +294,58 @@ def f(): yield (a, b) ``` -### `if PY2` blocks +### Python2 and old Python3.x blocks Availability: - `--py3-plus` is passed on the commandline. ```python # input -if six.PY2: # also understands `six.PY3` and `not` and `sys.version_info` +import sys +if sys.version_info < (3,): # also understands `six.PY2` (and `not`), `six.PY3` (and `not`) print('py2') else: print('py3') # output +import sys print('py3') ``` +Availability: +- `--py36-plus` will remove Python <= 3.5 only blocks +- `--py37-plus` will remove Python <= 3.6 only blocks +- so on and so forth + +```python +# using --py36-plus for this example +# input +import sys +if sys.version_info < (3, 6): + print('py3.5') +else: + print('py3.6+') + +if sys.version_info <= (3, 5): + print('py3.5') +else: + print('py3.6+') + +if sys.version_info >= (3, 6): + print('py3.6+') +else: + print('py3.5') + +# output +import sys +print('py3.6+') + +print('py3.6+') + +print('py3.6+') +``` + +Note that `if` blocks without an `else` will not be rewriten as it could introduce a syntax error. + ### remove `six` compatibility code Availability: diff --git a/pyupgrade/_plugins/versioned_branches.py b/pyupgrade/_plugins/versioned_branches.py index df66b744..6cc7a1c4 100644 --- a/pyupgrade/_plugins/versioned_branches.py +++ b/pyupgrade/_plugins/versioned_branches.py @@ -14,6 +14,7 @@ from pyupgrade._data import register from pyupgrade._data import State from pyupgrade._data import TokenFunc +from pyupgrade._data import Version from pyupgrade._token_helpers import Block @@ -77,6 +78,7 @@ def _eq(test: ast.Compare, n: int) -> bool: def _compare_to_3( test: ast.Compare, op: Union[Type[ast.cmpop], Tuple[Type[ast.cmpop], ...]], + minor: int = 0, ) -> bool: if not ( isinstance(test.ops[0], op) and @@ -87,9 +89,11 @@ def _compare_to_3( return False # checked above but mypy needs help - elts = cast('List[ast.Num]', test.comparators[0].elts) + ast_elts = cast('List[ast.Num]', test.comparators[0].elts) + # padding a 0 for compatibility with (3,) used as a spec + elts = tuple(e.n for e in ast_elts) + (0,) - return elts[0].n == 3 and all(n.n == 0 for n in elts[1:]) + return elts[:2] == (3, minor) and all(n == 0 for n in elts[2:]) @register(ast.If) @@ -98,8 +102,16 @@ def visit_If( node: ast.If, parent: ast.AST, ) -> Iterable[Tuple[Offset, TokenFunc]]: + + min_version: Version + if state.settings.min_version == (3,): + min_version = (3, 0) + else: + min_version = state.settings.min_version + assert len(min_version) >= 2 + if ( - state.settings.min_version >= (3,) and ( + min_version >= (3,) and ( # if six.PY2: is_name_attr(node.test, state.from_imports, 'six', ('PY2',)) or # if not six.PY3: @@ -114,6 +126,7 @@ def visit_If( ) ) or # sys.version_info == 2 or < (3,) + # or < (3, n) or <= (3, n) (with n= (3,) and ( + min_version >= (3,) and ( # if six.PY3: is_name_attr(node.test, state.from_imports, 'six', ('PY3',)) or # if not six.PY2: @@ -147,6 +164,8 @@ def visit_If( ) ) or # sys.version_info == 3 or >= (3,) or > (3,) + # sys.version_info >= (3, n) (with n<=m) + # or sys.version_info > (3, n) (with n (3, 5):\n' + ' 3+6\n' + 'else:\n' + ' 3-5\n', + + 'import sys\n' + '3+6\n', + id='sys.version_info > (3, 5)', + ), + pytest.param( + 'from sys import version_info\n' + 'if version_info > (3, 5):\n' + ' 3+6\n' + 'else:\n' + ' 3-5\n', + + 'from sys import version_info\n' + '3+6\n', + id='from sys import version_info, > (3, 5)', + ), + pytest.param( + 'import sys\n' + 'if sys.version_info >= (3, 6):\n' + ' 3+6\n' + 'else:\n' + ' 3-5\n', + + 'import sys\n' + '3+6\n', + id='sys.version_info >= (3, 6)', + ), + pytest.param( + 'from sys import version_info\n' + 'if version_info >= (3, 6):\n' + ' 3+6\n' + 'else:\n' + ' 3-5\n', + + 'from sys import version_info\n' + '3+6\n', + id='from sys import version_info, >= (3, 6)', + ), + pytest.param( + 'import sys\n' + 'if sys.version_info < (3, 6):\n' + ' 3-5\n' + 'else:\n' + ' 3+6\n', + + 'import sys\n' + '3+6\n', + id='sys.version_info < (3, 6)', + ), + pytest.param( + 'from sys import version_info\n' + 'if version_info < (3, 6):\n' + ' 3-5\n' + 'else:\n' + ' 3+6\n', + + 'from sys import version_info\n' + '3+6\n', + id='from sys import version_info, < (3, 6)', + ), + pytest.param( + 'import sys\n' + 'if sys.version_info <= (3, 5):\n' + ' 3-5\n' + 'else:\n' + ' 3+6\n', + + 'import sys\n' + '3+6\n', + id='sys.version_info <= (3, 5)', + ), + pytest.param( + 'from sys import version_info\n' + 'if version_info <= (3, 5):\n' + ' 3-5\n' + 'else:\n' + ' 3+6\n', + + 'from sys import version_info\n' + '3+6\n', + id='from sys import version_info, <= (3, 5)', + ), + ), +) +def test_fix_py3x_only_code(s, expected): + ret = _fix_plugins(s, settings=Settings(min_version=(3, 6))) + assert ret == expected + + +@pytest.mark.parametrize( + 's', + ( + # we timidly skip `if` without `else` as it could cause a SyntaxError + 'import sys' + 'if sys.version_info >= (3, 6):\n' + ' pass', + # here's the case where it causes a SyntaxError + 'import sys' + 'if True' + ' if sys.version_info >= (3, 6):\n' + ' pass\n', + # both branches are still relevant in the following cases + 'import sys\n' + 'if sys.version_info > (3, 7):\n' + ' 3-6\n' + 'else:\n' + ' 3+7\n', + + 'import sys\n' + 'if sys.version_info < (3, 7):\n' + ' 3-6\n' + 'else:\n' + ' 3+7\n', + + 'import sys\n' + 'if sys.version_info >= (3, 7):\n' + ' 3+7\n' + 'else:\n' + ' 3-6\n', + + 'import sys\n' + 'if sys.version_info <= (3, 7):\n' + ' 3-7\n' + 'else:\n' + ' 3+8\n', + + 'import sys\n' + 'if sys.version_info <= (3, 6):\n' + ' 3-6\n' + 'else:\n' + ' 3+7\n', + + 'import sys\n' + 'if sys.version_info > (3, 6):\n' + ' 3+7\n' + 'else:\n' + ' 3-6\n', + ), +) +def test_fix_py3x_only_noop(s): + assert _fix_plugins(s, settings=Settings(min_version=(3, 6))) == s From 486760aeedfeed700b8fa8b359ba91f77d540ec4 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 12 Sep 2021 18:46:58 -0400 Subject: [PATCH 15/54] v2.26.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 5c21a33a..3e4074f3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v2.25.1 + rev: v2.26.0 hooks: - id: pyupgrade args: [--py36-plus] diff --git a/README.md b/README.md index 45f54863..4adaebe2 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Sample `.pre-commit-config.yaml`: ```yaml - repo: https://github.com/asottile/pyupgrade - rev: v2.25.1 + rev: v2.26.0 hooks: - id: pyupgrade ``` diff --git a/setup.cfg b/setup.cfg index 39ffb2ac..92420c88 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pyupgrade -version = 2.25.1 +version = 2.26.0 description = A tool to automatically upgrade syntax for newer versions. long_description = file: README.md long_description_content_type = text/markdown From 9adb0c6658019a1a11a7f55fc7bfeab9c132e289 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Robert?= Date: Mon, 13 Sep 2021 21:11:18 +0200 Subject: [PATCH 16/54] BUG: fix broken tests for versioned_branches feature --- tests/features/versioned_branches_test.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/features/versioned_branches_test.py b/tests/features/versioned_branches_test.py index 3c865591..fa240aeb 100644 --- a/tests/features/versioned_branches_test.py +++ b/tests/features/versioned_branches_test.py @@ -545,6 +545,15 @@ def test_fix_py3_only_code(s, expected): '3+6\n', id='from sys import version_info, <= (3, 5)', ), + pytest.param( + 'import sys\n' + 'if sys.version_info >= (3, 6):\n' + ' pass', + + 'import sys\n' + 'pass', + id='sys.version_info >= (3, 6), noelse', + ), ), ) def test_fix_py3x_only_code(s, expected): @@ -555,15 +564,6 @@ def test_fix_py3x_only_code(s, expected): @pytest.mark.parametrize( 's', ( - # we timidly skip `if` without `else` as it could cause a SyntaxError - 'import sys' - 'if sys.version_info >= (3, 6):\n' - ' pass', - # here's the case where it causes a SyntaxError - 'import sys' - 'if True' - ' if sys.version_info >= (3, 6):\n' - ' pass\n', # both branches are still relevant in the following cases 'import sys\n' 'if sys.version_info > (3, 7):\n' From fd60bbecb9668e4392ade23dd8451b9f74d9d6c7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 17 Sep 2021 05:33:04 -0700 Subject: [PATCH 17/54] v2.26.0.post1 --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 92420c88..083b1375 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pyupgrade -version = 2.26.0 +version = 2.26.0.post1 description = A tool to automatically upgrade syntax for newer versions. long_description = file: README.md long_description_content_type = text/markdown From f4053ec5716a4da90da1943b15ec9d5f14119c20 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 22 Sep 2021 15:53:33 -0400 Subject: [PATCH 18/54] remove splatting of listcomp -> splat of generator --- README.md | 11 --- .../unpacking_argument_list_comprehensions.py | 26 ------ ...cking_argument_list_comprehensions_test.py | 80 ------------------- 3 files changed, 117 deletions(-) delete mode 100644 pyupgrade/_plugins/unpacking_argument_list_comprehensions.py delete mode 100644 tests/features/unpacking_argument_list_comprehensions_test.py diff --git a/README.md b/README.md index 4adaebe2..af4a069d 100644 --- a/README.md +++ b/README.md @@ -498,17 +498,6 @@ Availability: ``` -### Unpacking argument list comprehensions - -Availability: -- `--py3-plus` is passed on the commandline. - -```diff --foo(*[i for i in bar]) -+foo(*(i for i in bar)) -``` - - ### Rewrite `xml.etree.cElementTree` to `xml.etree.ElementTree` Availability: diff --git a/pyupgrade/_plugins/unpacking_argument_list_comprehensions.py b/pyupgrade/_plugins/unpacking_argument_list_comprehensions.py deleted file mode 100644 index 87bafd96..00000000 --- a/pyupgrade/_plugins/unpacking_argument_list_comprehensions.py +++ /dev/null @@ -1,26 +0,0 @@ -import ast -from typing import Iterable -from typing import Tuple - -from tokenize_rt import Offset - -from pyupgrade._ast_helpers import ast_to_offset -from pyupgrade._ast_helpers import is_async_listcomp -from pyupgrade._data import register -from pyupgrade._data import State -from pyupgrade._data import TokenFunc -from pyupgrade._token_helpers import replace_list_comp_brackets - - -@register(ast.Starred) -def visit_Starred( - state: State, - node: ast.Starred, - parent: ast.AST, -) -> Iterable[Tuple[Offset, TokenFunc]]: - if ( - state.settings.min_version >= (3,) and - isinstance(node.value, ast.ListComp) and - not is_async_listcomp(node.value) - ): - yield ast_to_offset(node.value), replace_list_comp_brackets diff --git a/tests/features/unpacking_argument_list_comprehensions_test.py b/tests/features/unpacking_argument_list_comprehensions_test.py deleted file mode 100644 index 7e41929d..00000000 --- a/tests/features/unpacking_argument_list_comprehensions_test.py +++ /dev/null @@ -1,80 +0,0 @@ -import pytest - -from pyupgrade._data import Settings -from pyupgrade._main import _fix_plugins - - -@pytest.mark.parametrize( - ('s', 'version'), - ( - pytest.param( - 'foo(*[i for i in bar])\n', - (2, 7), - id='Not Python3+', - ), - pytest.param( - '2*3', - (3,), - id='Multiplication star', - ), - pytest.param( - '2**3', - (3,), - id='Power star', - ), - pytest.param( - 'foo([i for i in bar])', - (3,), - id='List comp, no star', - ), - pytest.param( - 'foo(*bar)', - (3,), - id='Starred, no list comp', - ), - pytest.param( - 'foo(*[x async for x in bar])', - (3,), - id='async listcomp', - ), - ), -) -def test_fix_unpack_argument_list_comp_noop(s, version): - assert _fix_plugins(s, settings=Settings(min_version=version)) == s - - -@pytest.mark.parametrize( - ('s', 'expected'), - ( - pytest.param( - 'foo(*[i for i in bar])\n', - - 'foo(*(i for i in bar))\n', - - id='Starred list comprehension', - ), - pytest.param( - 'foo(\n' - ' *\n' - ' [i for i in bar]\n' - ' )\n', - - 'foo(\n' - ' *\n' - ' (i for i in bar)\n' - ' )\n', - - id='Multiline starred list comprehension', - ), - pytest.param( - 'foo(*[i for i in bar], qux, quox=None)\n', - - 'foo(*(i for i in bar), qux, quox=None)\n', - - id='Single line, including other args', - ), - ), -) -def test_fix_unpack_argument_list_comp(s, expected): - ret = _fix_plugins(s, settings=Settings((3,))) - assert ret == expected From 0f9d15b2e4162b74f02c59e37daa09013765fd3a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 22 Sep 2021 22:12:53 -0400 Subject: [PATCH 19/54] handle named escape sequences in format upgrades --- pyupgrade/_main.py | 39 +++++++++++++++++--------- pyupgrade/_plugins/percent_format.py | 12 ++------ pyupgrade/_string_helpers.py | 13 +++++++++ tests/features/format_literals_test.py | 15 ++++++++-- tests/features/fstrings_test.py | 7 +++-- tests/features/percent_format_test.py | 12 ++++++-- 6 files changed, 68 insertions(+), 30 deletions(-) diff --git a/pyupgrade/_main.py b/pyupgrade/_main.py index 291fc47a..2675619e 100644 --- a/pyupgrade/_main.py +++ b/pyupgrade/_main.py @@ -31,8 +31,10 @@ from pyupgrade._data import Settings from pyupgrade._data import Version from pyupgrade._data import visit +from pyupgrade._string_helpers import curly_escape from pyupgrade._string_helpers import is_ascii from pyupgrade._string_helpers import is_codec +from pyupgrade._string_helpers import NAMED_UNICODE_RE from pyupgrade._token_helpers import CLOSING from pyupgrade._token_helpers import KEYWORDS from pyupgrade._token_helpers import OPENING @@ -47,21 +49,34 @@ def parse_format(s: str) -> Tuple[DotFormatPart, ...]: - """Makes the empty string not a special case. In the stdlib, there's - loss of information (the type) on the empty string. - """ - parsed = tuple(_stdlib_parse_format(s)) - if not parsed: - return ((s, None, None, None),) - else: - return parsed + """handle named escape sequences""" + ret: List[DotFormatPart] = [] + + for part in NAMED_UNICODE_RE.split(s): + if NAMED_UNICODE_RE.fullmatch(part): + if not ret: + ret.append((part, None, None, None)) + else: + ret[-1] = (ret[-1][0] + part, None, None, None) + else: + first = True + for tup in _stdlib_parse_format(part): + if not first or not ret: + ret.append(tup) + else: + ret[-1] = (ret[-1][0] + tup[0], *tup[1:]) + first = False + + if not ret: + ret.append((s, None, None, None)) + + return tuple(ret) def unparse_parsed_string(parsed: Sequence[DotFormatPart]) -> str: def _convert_tup(tup: DotFormatPart) -> str: ret, field_name, format_spec, conversion = tup - ret = ret.replace('{', '{{') - ret = ret.replace('}', '}}') + ret = curly_escape(ret) if field_name is not None: ret += '{' + field_name if conversion: @@ -786,10 +801,6 @@ def _fix_py36_plus(contents_text: str, *, min_version: Version) -> str: return contents_text for i, token in reversed_enumerate(tokens): if token.offset in visitor.fstrings: - # TODO: handle \N escape sequences - if r'\N' in token.src: - continue - paren = i + 3 if tokens_to_src(tokens[i + 1:paren + 1]) != '.format(': continue diff --git a/pyupgrade/_plugins/percent_format.py b/pyupgrade/_plugins/percent_format.py index 1b0599df..c75d745a 100644 --- a/pyupgrade/_plugins/percent_format.py +++ b/pyupgrade/_plugins/percent_format.py @@ -18,6 +18,7 @@ from pyupgrade._data import register from pyupgrade._data import State from pyupgrade._data import TokenFunc +from pyupgrade._string_helpers import curly_escape from pyupgrade._token_helpers import KEYWORDS from pyupgrade._token_helpers import remove_brace from pyupgrade._token_helpers import victims @@ -120,7 +121,8 @@ def _simplify_conversion_flag(flag: str) -> str: def _percent_to_format(s: str) -> str: def _handle_part(part: PercentFormat) -> str: s, fmt = part - s = s.replace('{', '{{').replace('}', '}}') + s = curly_escape(s) + if fmt is None: return s else: @@ -155,10 +157,6 @@ def _fix_percent_format_tuple( *, node_right: ast.Tuple, ) -> None: - # TODO: handle \N escape sequences - if r'\N' in tokens[i].src: - return - # TODO: this is overly timid paren = i + 4 if tokens_to_src(tokens[i + 1:paren + 1]) != ' % (': @@ -181,10 +179,6 @@ def _fix_percent_format_dict( *, node_right: ast.Dict, ) -> None: - # TODO: handle \N escape sequences - if r'\N' in tokens[i].src: - return - seen_keys: Set[str] = set() keys = {} diff --git a/pyupgrade/_string_helpers.py b/pyupgrade/_string_helpers.py index cae45270..aac52cb0 100644 --- a/pyupgrade/_string_helpers.py +++ b/pyupgrade/_string_helpers.py @@ -1,4 +1,5 @@ import codecs +import re import string import sys @@ -8,6 +9,18 @@ def is_ascii(s: str) -> bool: return all(c in string.printable for c in s) +NAMED_UNICODE_RE = re.compile(r'(? str: + parts = NAMED_UNICODE_RE.split(s) + return ''.join( + part.replace('{', '{{').replace('}', '}}') + if not NAMED_UNICODE_RE.fullmatch(part) + else part + for part in parts + ) + def is_codec(encoding: str, name: str) -> bool: try: diff --git a/tests/features/format_literals_test.py b/tests/features/format_literals_test.py index bc00bd17..391fb40d 100644 --- a/tests/features/format_literals_test.py +++ b/tests/features/format_literals_test.py @@ -16,6 +16,14 @@ def test_roundtrip_text(s): assert unparse_parsed_string(parse_format(s)) == s +def test_parse_format_starts_with_named(): + # technically not possible since our string always starts with quotes + assert parse_format(r'\N{snowman} hi {0} hello') == ( + (r'\N{snowman} hi ', '0', '', None), + (' hello', None, None, None), + ) + + @pytest.mark.parametrize( ('s', 'expected'), ( @@ -49,8 +57,6 @@ def test_intentionally_not_round_trip(s, expected): "'{' '0}'.format(1)", # comment looks like placeholder but is not! '("{0}" # {1}\n"{2}").format(1, 2, 3)', - # TODO: this works by accident (extended escape treated as placeholder) - r'"\N{snowman} {}".format(1)', # don't touch f-strings (these are wrong but don't make it worse) 'f"{0}".format(a)', ), @@ -101,6 +107,11 @@ def test_format_literals_noop(s): ), # parenthesized string literals ('("{0}").format(1)', '("{}").format(1)'), + pytest.param( + r'"\N{snowman} {0}".format(1)', + r'"\N{snowman} {}".format(1)', + id='named escape sequence', + ), ), ) def test_format_literals(s, expected): diff --git a/tests/features/fstrings_test.py b/tests/features/fstrings_test.py index 3fd54c37..61c2cc1c 100644 --- a/tests/features/fstrings_test.py +++ b/tests/features/fstrings_test.py @@ -26,8 +26,6 @@ '"{:{}}".format(x, y)', '"{a[b]}".format(a=a)', '"{a.a[b]}".format(a=a)', - # TODO: handle \N escape sequences - r'"\N{snowman} {}".format(a)', # not enough placeholders / placeholders missing '"{}{}".format(a)', '"{a}{b}".format(a=a)', # backslashes and quotes cannot nest @@ -58,6 +56,11 @@ def test_fix_fstrings_noop(s): ('"{}{{}}{}".format(escaped, y)', 'f"{escaped}{{}}{y}"'), ('"{}{b}{}".format(a, c, b=b)', 'f"{a}{b}{c}"'), ('"{}".format(0x0)', 'f"{0x0}"'), + pytest.param( + r'"\N{snowman} {}".format(a)', + r'f"\N{snowman} {a}"', + id='named escape sequences', + ), # TODO: poor man's f-strings? # '"{foo}".format(**locals())' ), diff --git a/tests/features/percent_format_test.py b/tests/features/percent_format_test.py index 0d620ce0..536435e9 100644 --- a/tests/features/percent_format_test.py +++ b/tests/features/percent_format_test.py @@ -178,9 +178,6 @@ def test_simplify_conversion_flag(s, expected): '"%(and)s" % {"and": 2}', # invalid string formats '"%" % {}', '"%(hi)" % {}', '"%2" % {}', - # TODO: handle \N escape sequences - r'"%s \N{snowman}" % (a,)', - r'"%(foo)s \N{snowman}" % {"foo": 1}', ), ) def test_percent_format_noop(s): @@ -223,6 +220,15 @@ def test_percent_format_noop_if_bug_16806(): # dict ('"%(k)s" % {"k": "v"}', '"{k}".format(k="v")'), ('"%(to_list)s" % {"to_list": []}', '"{to_list}".format(to_list=[])'), + # \N escapes + ( + r'"%s \N{snowman}" % (a,)', + r'"{} \N{snowman}".format(a)', + ), + ( + r'"%(foo)s \N{snowman}" % {"foo": 1}', + r'"{foo} \N{snowman}".format(foo=1)', + ), ), ) def test_percent_format(s, expected): From 9c4075819aca56a27931cfd18082d0d2b2e3d02e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 22 Sep 2021 22:23:06 -0400 Subject: [PATCH 20/54] v2.27.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 3e4074f3..475449d3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v2.26.0 + rev: v2.27.0 hooks: - id: pyupgrade args: [--py36-plus] diff --git a/README.md b/README.md index af4a069d..39720f7c 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Sample `.pre-commit-config.yaml`: ```yaml - repo: https://github.com/asottile/pyupgrade - rev: v2.26.0 + rev: v2.27.0 hooks: - id: pyupgrade ``` diff --git a/setup.cfg b/setup.cfg index 083b1375..136984b7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pyupgrade -version = 2.26.0.post1 +version = 2.27.0 description = A tool to automatically upgrade syntax for newer versions. long_description = file: README.md long_description_content_type = text/markdown From 2fce4a164446e4b1775ab36f150d4c65c0bc3315 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 10 Nov 2020 16:02:30 -0800 Subject: [PATCH 21/54] fix invalid dedent with comment after block --- pyupgrade/_token_helpers.py | 12 +++++++++--- tests/features/versioned_branches_test.py | 14 ++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/pyupgrade/_token_helpers.py b/pyupgrade/_token_helpers.py index 66232bda..9669ffa8 100644 --- a/pyupgrade/_token_helpers.py +++ b/pyupgrade/_token_helpers.py @@ -195,7 +195,9 @@ def _minimum_indent(self, tokens: List[Token]) -> int: for i in range(self.block, self.end): if ( tokens[i - 1].name in ('NL', 'NEWLINE') and - tokens[i].name in ('INDENT', UNIMPORTANT_WS) + tokens[i].name in ('INDENT', UNIMPORTANT_WS) and + # comments can have arbitrary indentation so ignore them + tokens[i + 1].name != 'COMMENT' ): token_indent = len(tokens[i].src) if block_indent is None: @@ -209,13 +211,17 @@ def _minimum_indent(self, tokens: List[Token]) -> int: def dedent(self, tokens: List[Token]) -> None: if self.line: return - diff = self._minimum_indent(tokens) - self._initial_indent(tokens) + initial_indent = self._initial_indent(tokens) + diff = self._minimum_indent(tokens) - initial_indent for i in range(self.block, self.end): if ( tokens[i - 1].name in ('DEDENT', 'NL', 'NEWLINE') and tokens[i].name in ('INDENT', UNIMPORTANT_WS) ): - tokens[i] = tokens[i]._replace(src=tokens[i].src[diff:]) + # make sure we preserve *at least* the initial indent + s = tokens[i].src + s = s[:initial_indent] + s[initial_indent + diff:] + tokens[i] = tokens[i]._replace(src=s) def replace_condition(self, tokens: List[Token], new: List[Token]) -> None: start = self.start diff --git a/tests/features/versioned_branches_test.py b/tests/features/versioned_branches_test.py index fa240aeb..2cd269e0 100644 --- a/tests/features/versioned_branches_test.py +++ b/tests/features/versioned_branches_test.py @@ -430,6 +430,20 @@ def test_fix_py2_block_noop(s): id='elif six.PY3 no else, indented', ), + pytest.param( + 'if True:\n' + ' if sys.version_info > (3,):\n' + ' print(3)\n' + ' # comment\n' + ' print(2+3)\n', + + 'if True:\n' + ' print(3)\n' + ' # comment\n' + ' print(2+3)\n', + + id='comment after dedented block', + ), ), ) def test_fix_py2_blocks(s, expected): From ca736a0107d1fe4710a2229e0e26c490350b6263 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 10 Nov 2020 15:56:55 -0800 Subject: [PATCH 22/54] fix rewrite causing syntax error when the first arg has newlines --- pyupgrade/_token_helpers.py | 18 ++++++++++++++++++ tests/features/six_test.py | 22 ++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/pyupgrade/_token_helpers.py b/pyupgrade/_token_helpers.py index 9669ffa8..44d9b575 100644 --- a/pyupgrade/_token_helpers.py +++ b/pyupgrade/_token_helpers.py @@ -396,6 +396,16 @@ def arg_str(tokens: List[Token], start: int, end: int) -> str: return tokens_to_src(tokens[start:end]).strip() +def _arg_contains_newline(tokens: List[Token], start: int, end: int) -> bool: + while tokens[start].name in {'NL', 'NEWLINE', UNIMPORTANT_WS}: + start += 1 + for i in range(start, end): + if tokens[i].name in {'NL', 'NEWLINE'}: + return True + else: + return False + + def replace_call( tokens: List[Token], start: int, @@ -409,6 +419,14 @@ def replace_call( for paren in parens: arg_strs[paren] = f'({arg_strs[paren]})' + # there are a few edge cases which cause syntax errors when the first + # argument contains newlines (especially when moved outside of a natural + # contiunuation context) + if _arg_contains_newline(tokens, *args[0]) and 0 not in parens: + # this attempts to preserve more of the whitespace by using the + # original non-stripped argument string + arg_strs[0] = f'({tokens_to_src(tokens[slice(*args[0])])})' + start_rest = args[0][1] + 1 while ( start_rest < end and diff --git a/tests/features/six_test.py b/tests/features/six_test.py index 71d41c8b..2b87b8ff 100644 --- a/tests/features/six_test.py +++ b/tests/features/six_test.py @@ -347,6 +347,28 @@ def test_fix_six_noop(s): '(x < y).values()', id='needs parentehsizing for Compare', ), + pytest.param( + 'x = six.itervalues(\n' + ' # comment\n' + ' x\n' + ')', + 'x = (\n' + ' # comment\n' + ' x\n' + ').values()', + id='multiline first argument with comment', + ), + pytest.param( + 'x = six.itervalues(\n' + ' # comment\n' + ' x,\n' + ')', + # TODO: ideally this would preserve whitespace better + 'x = (\n' + ' # comment\n' + ' x).values()', + id='multiline first argument with comment, trailing comma', + ), ), ) def test_fix_six(s, expected): From 4436b63c4cef5d1f0048acbc35caef852151a24f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 10 Nov 2020 16:04:51 -0800 Subject: [PATCH 23/54] fix raise_from with multi lines / trailing commas --- pyupgrade/_plugins/six_calls.py | 2 +- tests/features/six_test.py | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/pyupgrade/_plugins/six_calls.py b/pyupgrade/_plugins/six_calls.py index 3fd30cd7..1bb50fae 100644 --- a/pyupgrade/_plugins/six_calls.py +++ b/pyupgrade/_plugins/six_calls.py @@ -51,7 +51,7 @@ 'assertRegex': '{args[0]}.assertRegex({rest})', } SIX_INT2BYTE_TMPL = 'bytes(({args[0]},))' -RAISE_FROM_TMPL = 'raise {args[0]} from {rest}' +RAISE_FROM_TMPL = 'raise {args[0]} from {args[1]}' RERAISE_TMPL = 'raise' RERAISE_2_TMPL = 'raise {args[1]}.with_traceback(None)' RERAISE_3_TMPL = 'raise {args[1]}.with_traceback({args[2]})' diff --git a/tests/features/six_test.py b/tests/features/six_test.py index 2b87b8ff..e1f6cd88 100644 --- a/tests/features/six_test.py +++ b/tests/features/six_test.py @@ -126,6 +126,16 @@ def test_fix_six_noop(s): 'six.raise_from(exc, exc_from)\n', 'raise exc from exc_from\n', ), + pytest.param( + 'six.raise_from(\n' + ' e,\n' + ' f,\n' + ')', + + 'raise e from f', + + id='six raise_from across multiple lines', + ), ( 'six.reraise(tp, exc, tb)\n', 'raise exc.with_traceback(tb)\n', From 820f9ae7e2d813fe6c61918d0bcb113b7cd713d5 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 30 Jan 2021 11:46:05 -0800 Subject: [PATCH 24/54] Revert "Revert "Merge pull request #317 from asottile/old_super"" This reverts commit 2719335fa7bdb582b35ac90547a0f763d4225036. --- pyupgrade/_ast_helpers.py | 44 +++++++++++++++ pyupgrade/_plugins/legacy.py | 90 ++++++++++++++++--------------- tests/ast_helpers_test.py | 19 +++++++ tests/features/super_test.py | 74 +++++++++++++++++++++++++ tests/features/yield_from_test.py | 19 ------- 5 files changed, 185 insertions(+), 61 deletions(-) create mode 100644 tests/ast_helpers_test.py diff --git a/pyupgrade/_ast_helpers.py b/pyupgrade/_ast_helpers.py index eb4ba066..2f5c05ae 100644 --- a/pyupgrade/_ast_helpers.py +++ b/pyupgrade/_ast_helpers.py @@ -1,8 +1,12 @@ import ast import warnings +from typing import Any from typing import Container from typing import Dict +from typing import Iterable from typing import Set +from typing import Tuple +from typing import Type from typing import Union from tokenize_rt import Offset @@ -57,3 +61,43 @@ def is_async_listcomp(node: ast.ListComp) -> bool: any(gen.is_async for gen in node.generators) or contains_await(node) ) + + +def _all_isinstance( + vals: Iterable[Any], + tp: Union[Type[Any], Tuple[Type[Any], ...]], +) -> bool: + return all(isinstance(v, tp) for v in vals) + + +def _fields_same(n1: ast.AST, n2: ast.AST) -> bool: + for (a1, v1), (a2, v2) in zip(ast.iter_fields(n1), ast.iter_fields(n2)): + # ignore ast attributes, they'll be covered by walk + if a1 != a2: + return False + elif _all_isinstance((v1, v2), ast.AST): + continue + elif _all_isinstance((v1, v2), (list, tuple)): + if len(v1) != len(v2): + return False + # ignore sequences which are all-ast, they'll be covered by walk + elif _all_isinstance(v1, ast.AST) and _all_isinstance(v2, ast.AST): + continue + elif v1 != v2: + return False + elif v1 != v2: + return False + return True + + +def targets_same(node1: ast.AST, node2: ast.AST) -> bool: + for t1, t2 in zip(ast.walk(node1), ast.walk(node2)): + # ignore `ast.Load` / `ast.Store` + if _all_isinstance((t1, t2), ast.expr_context): + continue + elif type(t1) != type(t2): + return False + elif not _fields_same(t1, t2): + return False + else: + return True diff --git a/pyupgrade/_plugins/legacy.py b/pyupgrade/_plugins/legacy.py index 321ffba0..e8443065 100644 --- a/pyupgrade/_plugins/legacy.py +++ b/pyupgrade/_plugins/legacy.py @@ -2,28 +2,28 @@ import collections import contextlib import functools -from typing import Any from typing import Dict from typing import Generator from typing import Iterable from typing import List from typing import Set from typing import Tuple -from typing import Type -from typing import Union from tokenize_rt import Offset from tokenize_rt import Token from tokenize_rt import tokens_to_src from pyupgrade._ast_helpers import ast_to_offset +from pyupgrade._ast_helpers import targets_same from pyupgrade._data import register from pyupgrade._data import State from pyupgrade._data import TokenFunc from pyupgrade._token_helpers import Block from pyupgrade._token_helpers import find_and_replace_call from pyupgrade._token_helpers import find_block_start +from pyupgrade._token_helpers import find_open_paren from pyupgrade._token_helpers import find_token +from pyupgrade._token_helpers import parse_call_args FUNC_TYPES = (ast.Lambda, ast.FunctionDef, ast.AsyncFunctionDef) @@ -36,44 +36,27 @@ def _fix_yield(i: int, tokens: List[Token]) -> None: tokens[i:block.end] = [Token('CODE', f'yield from {container}\n')] -def _all_isinstance( - vals: Iterable[Any], - tp: Union[Type[Any], Tuple[Type[Any], ...]], -) -> bool: - return all(isinstance(v, tp) for v in vals) - - -def _fields_same(n1: ast.AST, n2: ast.AST) -> bool: - for (a1, v1), (a2, v2) in zip(ast.iter_fields(n1), ast.iter_fields(n2)): - # ignore ast attributes, they'll be covered by walk - if a1 != a2: - return False - elif _all_isinstance((v1, v2), ast.AST): - continue - elif _all_isinstance((v1, v2), (list, tuple)): - if len(v1) != len(v2): - return False - # ignore sequences which are all-ast, they'll be covered by walk - elif _all_isinstance(v1, ast.AST) and _all_isinstance(v2, ast.AST): - continue - elif v1 != v2: - return False - elif v1 != v2: - return False - return True - - -def _targets_same(target: ast.AST, yield_value: ast.AST) -> bool: - for t1, t2 in zip(ast.walk(target), ast.walk(yield_value)): - # ignore `ast.Load` / `ast.Store` - if _all_isinstance((t1, t2), ast.expr_context): - continue - elif type(t1) != type(t2): - return False - elif not _fields_same(t1, t2): - return False +def _fix_old_super(i: int, tokens: List[Token]) -> None: + j = find_open_paren(tokens, i) + k = j - 1 + while tokens[k].src != '.': + k -= 1 + func_args, end = parse_call_args(tokens, j) + # remove the first argument + if len(func_args) == 1: + del tokens[func_args[0][0]:func_args[0][0] + 1] else: - return True + del tokens[func_args[0][0]:func_args[1][0] + 1] + tokens[i:k] = [Token('CODE', 'super()')] + + +def _is_simple_base(base: ast.AST) -> bool: + return ( + isinstance(base, ast.Name) or ( + isinstance(base, ast.Attribute) and + _is_simple_base(base.value) + ) + ) class Scope: @@ -92,6 +75,7 @@ class Visitor(ast.NodeVisitor): def __init__(self) -> None: self._scopes: List[Scope] = [] self.super_offsets: Set[Offset] = set() + self.old_super_offsets: Set[Offset] = set() self.yield_offsets: Set[Offset] = set() @contextlib.contextmanager @@ -137,7 +121,6 @@ def visit_Call(self, node: ast.Call) -> None: len(node.args) == 2 and isinstance(node.args[0], ast.Name) and isinstance(node.args[1], ast.Name) and - # there are at least two scopes len(self._scopes) >= 2 and # the second to last scope is the class in arg1 isinstance(self._scopes[-2].node, ast.ClassDef) and @@ -148,6 +131,26 @@ def visit_Call(self, node: ast.Call) -> None: node.args[1].id == self._scopes[-1].node.args.args[0].arg ): self.super_offsets.add(ast_to_offset(node)) + elif ( + len(self._scopes) >= 2 and + # last stack is a function whose first argument is the first + # argument of this function + len(node.args) >= 1 and + isinstance(node.args[0], ast.Name) and + isinstance(self._scopes[-1].node, FUNC_TYPES) and + len(self._scopes[-1].node.args.args) >= 1 and + node.args[0].id == self._scopes[-1].node.args.args[0].arg and + # the function is an attribute of the contained class name + isinstance(node.func, ast.Attribute) and + isinstance(self._scopes[-2].node, ast.ClassDef) and + len(self._scopes[-2].node.bases) == 1 and + _is_simple_base(self._scopes[-2].node.bases[0]) and + targets_same( + self._scopes[-2].node.bases[0], + node.func.value, + ) + ): + self.old_super_offsets.add(ast_to_offset(node)) self.generic_visit(node) @@ -159,7 +162,7 @@ def visit_For(self, node: ast.For) -> None: isinstance(node.body[0], ast.Expr) and isinstance(node.body[0].value, ast.Yield) and node.body[0].value.value is not None and - _targets_same(node.target, node.body[0].value.value) and + targets_same(node.target, node.body[0].value.value) and not node.orelse ): offset = ast_to_offset(node) @@ -198,5 +201,8 @@ def visit_Module( for offset in visitor.super_offsets: yield offset, super_func + for offset in visitor.old_super_offsets: + yield offset, _fix_old_super + for offset in visitor.yield_offsets: yield offset, _fix_yield diff --git a/tests/ast_helpers_test.py b/tests/ast_helpers_test.py new file mode 100644 index 00000000..282ea729 --- /dev/null +++ b/tests/ast_helpers_test.py @@ -0,0 +1,19 @@ +import ast + +from pyupgrade._ast_helpers import _fields_same +from pyupgrade._ast_helpers import targets_same + + +def test_targets_same(): + assert targets_same(ast.parse('global a, b'), ast.parse('global a, b')) + assert not targets_same(ast.parse('global a'), ast.parse('global b')) + + +def _get_body(expr): + body = ast.parse(expr).body[0] + assert isinstance(body, ast.Expr) + return body.value + + +def test_fields_same(): + assert not _fields_same(_get_body('x'), _get_body('1')) diff --git a/tests/features/super_test.py b/tests/features/super_test.py index ee558a6d..6e2103c9 100644 --- a/tests/features/super_test.py +++ b/tests/features/super_test.py @@ -122,3 +122,77 @@ def test_fix_super_noop(s): ) def test_fix_super(s, expected): assert _fix_plugins(s, settings=Settings(min_version=(3,))) == expected + + +@pytest.mark.parametrize( + 's', + ( + pytest.param( + 'class C(B):\n' + ' def f(self):\n' + ' B.f(notself)\n', + id='old style super, first argument is not first function arg', + ), + pytest.param( + 'class C(B1, B2):\n' + ' def f(self):\n' + ' B1.f(self)\n', + # TODO: is this safe to rewrite? I don't think so + id='old-style super, multiple inheritance first class', + ), + pytest.param( + 'class C(B1, B2):\n' + ' def f(self):\n' + ' B2.f(self)\n', + # TODO: is this safe to rewrite? I don't think so + id='old-style super, multiple inheritance not-first class', + ), + pytest.param( + 'class C(Base):\n' + ' def f(self):\n' + ' return [Base.f(self) for _ in ()]\n', + id='super in comprehension', + ), + pytest.param( + 'class C(Base):\n' + ' def f(self):\n' + ' def g():\n' + ' Base.f(self)\n' + ' g()\n', + id='super in nested functions', + ), + pytest.param( + 'class C(not_simple()):\n' + ' def f(self):\n' + ' not_simple().f(self)\n', + id='not a simple base', + ), + pytest.param( + 'class C(a().b):\n' + ' def f(self):\n' + ' a().b.f(self)\n', + id='non simple attribute base', + ), + ), +) +def test_old_style_class_super_noop(s): + assert _fix_plugins(s, settings=Settings(min_version=(3,))) == s + + +@pytest.mark.parametrize( + ('s', 'expected'), + ( + ( + 'class C(B):\n' + ' def f(self):\n' + ' B.f(self)\n' + ' B.f(self, arg, arg)\n', + 'class C(B):\n' + ' def f(self):\n' + ' super().f()\n' + ' super().f(arg, arg)\n', + ), + ), +) +def test_old_style_class_super(s, expected): + assert _fix_plugins(s, settings=Settings(min_version=(3,))) == expected diff --git a/tests/features/yield_from_test.py b/tests/features/yield_from_test.py index d3d12326..dc040993 100644 --- a/tests/features/yield_from_test.py +++ b/tests/features/yield_from_test.py @@ -1,11 +1,7 @@ -import ast - import pytest from pyupgrade._data import Settings from pyupgrade._main import _fix_plugins -from pyupgrade._plugins.legacy import _fields_same -from pyupgrade._plugins.legacy import _targets_same @pytest.mark.parametrize( @@ -215,18 +211,3 @@ def test_fix_yield_from(s, expected): ) def test_fix_yield_from_noop(s): assert _fix_plugins(s, settings=Settings(min_version=(3,))) == s - - -def test_targets_same(): - assert _targets_same(ast.parse('global a, b'), ast.parse('global a, b')) - assert not _targets_same(ast.parse('global a'), ast.parse('global b')) - - -def _get_body(expr): - body = ast.parse(expr).body[0] - assert isinstance(body, ast.Expr) - return body.value - - -def test_fields_same(): - assert not _fields_same(_get_body('x'), _get_body('1')) From eadaf860db8819732de22e8b4fc610577db7be9a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 30 Jan 2021 11:56:46 -0800 Subject: [PATCH 25/54] Fix bug with calling different superclass method --- pyupgrade/_plugins/legacy.py | 11 +++++++---- tests/features/super_test.py | 9 +++++++++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/pyupgrade/_plugins/legacy.py b/pyupgrade/_plugins/legacy.py index e8443065..93f6eff5 100644 --- a/pyupgrade/_plugins/legacy.py +++ b/pyupgrade/_plugins/legacy.py @@ -26,6 +26,7 @@ from pyupgrade._token_helpers import parse_call_args FUNC_TYPES = (ast.Lambda, ast.FunctionDef, ast.AsyncFunctionDef) +NON_LAMBDA_FUNC_TYPES = (ast.FunctionDef, ast.AsyncFunctionDef) def _fix_yield(i: int, tokens: List[Token]) -> None: @@ -132,16 +133,18 @@ def visit_Call(self, node: ast.Call) -> None: ): self.super_offsets.add(ast_to_offset(node)) elif ( + # base.funcname(funcarg1, ...) + isinstance(node.func, ast.Attribute) and + len(node.args) >= 1 and + isinstance(node.args[0], ast.Name) and len(self._scopes) >= 2 and # last stack is a function whose first argument is the first # argument of this function - len(node.args) >= 1 and - isinstance(node.args[0], ast.Name) and - isinstance(self._scopes[-1].node, FUNC_TYPES) and + isinstance(self._scopes[-1].node, NON_LAMBDA_FUNC_TYPES) and + node.func.attr == self._scopes[-1].node.name and len(self._scopes[-1].node.args.args) >= 1 and node.args[0].id == self._scopes[-1].node.args.args[0].arg and # the function is an attribute of the contained class name - isinstance(node.func, ast.Attribute) and isinstance(self._scopes[-2].node, ast.ClassDef) and len(self._scopes[-2].node.bases) == 1 and _is_simple_base(self._scopes[-2].node.bases[0]) and diff --git a/tests/features/super_test.py b/tests/features/super_test.py index 6e2103c9..0149a7fb 100644 --- a/tests/features/super_test.py +++ b/tests/features/super_test.py @@ -173,6 +173,15 @@ def test_fix_super(s, expected): ' a().b.f(self)\n', id='non simple attribute base', ), + pytest.param( + 'class C:\n' + ' @classmethod\n' + ' def make(cls, instance):\n' + ' ...\n' + 'class D(C):\n' + ' def find(self):\n' + ' return C.make(self)\n', + ), ), ) def test_old_style_class_super_noop(s): From dd1fe7fae1ca4decc4c03445beecbe32fd86510a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 25 Jul 2020 14:27:33 -0700 Subject: [PATCH 26/54] fix super replacement of multiple lines --- pyupgrade/_plugins/legacy.py | 26 ++++++-------------------- tests/features/super_test.py | 15 +++++++++++++++ 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/pyupgrade/_plugins/legacy.py b/pyupgrade/_plugins/legacy.py index 93f6eff5..8de3fa75 100644 --- a/pyupgrade/_plugins/legacy.py +++ b/pyupgrade/_plugins/legacy.py @@ -21,9 +21,7 @@ from pyupgrade._token_helpers import Block from pyupgrade._token_helpers import find_and_replace_call from pyupgrade._token_helpers import find_block_start -from pyupgrade._token_helpers import find_open_paren from pyupgrade._token_helpers import find_token -from pyupgrade._token_helpers import parse_call_args FUNC_TYPES = (ast.Lambda, ast.FunctionDef, ast.AsyncFunctionDef) NON_LAMBDA_FUNC_TYPES = (ast.FunctionDef, ast.AsyncFunctionDef) @@ -37,20 +35,6 @@ def _fix_yield(i: int, tokens: List[Token]) -> None: tokens[i:block.end] = [Token('CODE', f'yield from {container}\n')] -def _fix_old_super(i: int, tokens: List[Token]) -> None: - j = find_open_paren(tokens, i) - k = j - 1 - while tokens[k].src != '.': - k -= 1 - func_args, end = parse_call_args(tokens, j) - # remove the first argument - if len(func_args) == 1: - del tokens[func_args[0][0]:func_args[0][0] + 1] - else: - del tokens[func_args[0][0]:func_args[1][0] + 1] - tokens[i:k] = [Token('CODE', 'super()')] - - def _is_simple_base(base: ast.AST) -> bool: return ( isinstance(base, ast.Name) or ( @@ -76,7 +60,7 @@ class Visitor(ast.NodeVisitor): def __init__(self) -> None: self._scopes: List[Scope] = [] self.super_offsets: Set[Offset] = set() - self.old_super_offsets: Set[Offset] = set() + self.old_super_offsets: Set[Tuple[Offset, str]] = set() self.yield_offsets: Set[Offset] = set() @contextlib.contextmanager @@ -153,7 +137,7 @@ def visit_Call(self, node: ast.Call) -> None: node.func.value, ) ): - self.old_super_offsets.add(ast_to_offset(node)) + self.old_super_offsets.add((ast_to_offset(node), node.func.attr)) self.generic_visit(node) @@ -204,8 +188,10 @@ def visit_Module( for offset in visitor.super_offsets: yield offset, super_func - for offset in visitor.old_super_offsets: - yield offset, _fix_old_super + for offset, func_name in visitor.old_super_offsets: + template = f'super().{func_name}({{rest}})' + callback = functools.partial(find_and_replace_call, template=template) + yield offset, callback for offset in visitor.yield_offsets: yield offset, _fix_yield diff --git a/tests/features/super_test.py b/tests/features/super_test.py index 0149a7fb..124f87df 100644 --- a/tests/features/super_test.py +++ b/tests/features/super_test.py @@ -201,6 +201,21 @@ def test_old_style_class_super_noop(s): ' super().f()\n' ' super().f(arg, arg)\n', ), + pytest.param( + 'class C(B):\n' + ' def f(self, a):\n' + ' B.f(\n' + ' self,\n' + ' a,\n' + ' )\n', + + 'class C(B):\n' + ' def f(self, a):\n' + ' super().f(\n' + ' a,\n' + ' )\n', + id='multi-line super call', + ), ), ) def test_old_style_class_super(s, expected): From 1a76829caa376722ccf8d02c6d2dd33bbe30a94f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 25 Sep 2021 16:03:36 -0400 Subject: [PATCH 27/54] don't rewrite old super calls for __new__ --- pyupgrade/_plugins/legacy.py | 1 + tests/features/super_test.py | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/pyupgrade/_plugins/legacy.py b/pyupgrade/_plugins/legacy.py index 8de3fa75..f7ba3763 100644 --- a/pyupgrade/_plugins/legacy.py +++ b/pyupgrade/_plugins/legacy.py @@ -126,6 +126,7 @@ def visit_Call(self, node: ast.Call) -> None: # argument of this function isinstance(self._scopes[-1].node, NON_LAMBDA_FUNC_TYPES) and node.func.attr == self._scopes[-1].node.name and + node.func.attr != '__new__' and len(self._scopes[-1].node.args.args) >= 1 and node.args[0].id == self._scopes[-1].node.args.args[0].arg and # the function is an attribute of the contained class name diff --git a/tests/features/super_test.py b/tests/features/super_test.py index 124f87df..8e3be14f 100644 --- a/tests/features/super_test.py +++ b/tests/features/super_test.py @@ -182,6 +182,12 @@ def test_fix_super(s, expected): ' def find(self):\n' ' return C.make(self)\n', ), + pytest.param( + 'class C(tuple):\n' + ' def __new__(cls, arg):\n' + ' return tuple.__new__(cls, (arg,))\n', + id='super() does not work properly for __new__', + ), ), ) def test_old_style_class_super_noop(s): From a2f517f0103c1f74bffbc06be510bcec4cd181ec Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 25 Sep 2021 17:35:49 -0400 Subject: [PATCH 28/54] v2.28.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 475449d3..81d1c9f2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v2.27.0 + rev: v2.28.0 hooks: - id: pyupgrade args: [--py36-plus] diff --git a/README.md b/README.md index 39720f7c..6665fba9 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Sample `.pre-commit-config.yaml`: ```yaml - repo: https://github.com/asottile/pyupgrade - rev: v2.27.0 + rev: v2.28.0 hooks: - id: pyupgrade ``` diff --git a/setup.cfg b/setup.cfg index 136984b7..e209cbf5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pyupgrade -version = 2.27.0 +version = 2.28.0 description = A tool to automatically upgrade syntax for newer versions. long_description = file: README.md long_description_content_type = text/markdown From 76f6a740742843b03725d10e579cb8470f07699d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 27 Sep 2021 15:15:53 -0400 Subject: [PATCH 29/54] don't rewrite old-super for staticmethods --- pyupgrade/_plugins/legacy.py | 11 +++++++++++ tests/features/super_test.py | 18 ++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/pyupgrade/_plugins/legacy.py b/pyupgrade/_plugins/legacy.py index f7ba3763..3ae702a2 100644 --- a/pyupgrade/_plugins/legacy.py +++ b/pyupgrade/_plugins/legacy.py @@ -8,6 +8,7 @@ from typing import List from typing import Set from typing import Tuple +from typing import Union from tokenize_rt import Offset from tokenize_rt import Token @@ -25,6 +26,7 @@ FUNC_TYPES = (ast.Lambda, ast.FunctionDef, ast.AsyncFunctionDef) NON_LAMBDA_FUNC_TYPES = (ast.FunctionDef, ast.AsyncFunctionDef) +NonLambdaFuncTypes_T = Union[ast.FunctionDef, ast.AsyncFunctionDef] def _fix_yield(i: int, tokens: List[Token]) -> None: @@ -44,6 +46,14 @@ def _is_simple_base(base: ast.AST) -> bool: ) +def _is_staticmethod_decorated(node: NonLambdaFuncTypes_T) -> bool: + for decorator in node.decorator_list: + if isinstance(decorator, ast.Name) and decorator.id == 'staticmethod': + return True + else: + return False + + class Scope: def __init__(self, node: ast.AST) -> None: self.node = node @@ -127,6 +137,7 @@ def visit_Call(self, node: ast.Call) -> None: isinstance(self._scopes[-1].node, NON_LAMBDA_FUNC_TYPES) and node.func.attr == self._scopes[-1].node.name and node.func.attr != '__new__' and + not _is_staticmethod_decorated(self._scopes[-1].node) and len(self._scopes[-1].node.args.args) >= 1 and node.args[0].id == self._scopes[-1].node.args.args[0].arg and # the function is an attribute of the contained class name diff --git a/tests/features/super_test.py b/tests/features/super_test.py index 8e3be14f..e58ffeab 100644 --- a/tests/features/super_test.py +++ b/tests/features/super_test.py @@ -188,6 +188,13 @@ def test_fix_super(s, expected): ' return tuple.__new__(cls, (arg,))\n', id='super() does not work properly for __new__', ), + pytest.param( + 'class C(B):\n' + ' @staticmethod\n' + ' def f(arg):\n' + ' return B.f(arg)\n', + id='skip staticmethod', + ), ), ) def test_old_style_class_super_noop(s): @@ -207,6 +214,17 @@ def test_old_style_class_super_noop(s): ' super().f()\n' ' super().f(arg, arg)\n', ), + pytest.param( + 'class C(B):\n' + ' @classmethod\n' + ' def f(cls):\n' + ' B.f(cls)\n', + 'class C(B):\n' + ' @classmethod\n' + ' def f(cls):\n' + ' super().f()\n', + id='@classmethod', + ), pytest.param( 'class C(B):\n' ' def f(self, a):\n' From d9505624a623bd2301df3e1445ca38d293f6f16a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 27 Sep 2021 15:19:36 -0400 Subject: [PATCH 30/54] v2.28.1 --- .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 81d1c9f2..8397a38c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v2.28.0 + rev: v2.28.1 hooks: - id: pyupgrade args: [--py36-plus] diff --git a/README.md b/README.md index 6665fba9..e01d38f2 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Sample `.pre-commit-config.yaml`: ```yaml - repo: https://github.com/asottile/pyupgrade - rev: v2.28.0 + rev: v2.28.1 hooks: - id: pyupgrade ``` diff --git a/setup.cfg b/setup.cfg index e209cbf5..4993d089 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pyupgrade -version = 2.28.0 +version = 2.28.1 description = A tool to automatically upgrade syntax for newer versions. long_description = file: README.md long_description_content_type = text/markdown From 9b8a728a29e28878ed860e5ea8aaed75f3412a0f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 28 Sep 2021 08:02:53 -0700 Subject: [PATCH 31/54] Revert "Merge pull request #545 from asottile/skip-staticmethod" This reverts commit 8768d42facdaa1ef771a5a689b11329a54afd00a, reversing changes made to a2f517f0103c1f74bffbc06be510bcec4cd181ec. --- pyupgrade/_plugins/legacy.py | 11 ----------- tests/features/super_test.py | 18 ------------------ 2 files changed, 29 deletions(-) diff --git a/pyupgrade/_plugins/legacy.py b/pyupgrade/_plugins/legacy.py index 3ae702a2..f7ba3763 100644 --- a/pyupgrade/_plugins/legacy.py +++ b/pyupgrade/_plugins/legacy.py @@ -8,7 +8,6 @@ from typing import List from typing import Set from typing import Tuple -from typing import Union from tokenize_rt import Offset from tokenize_rt import Token @@ -26,7 +25,6 @@ FUNC_TYPES = (ast.Lambda, ast.FunctionDef, ast.AsyncFunctionDef) NON_LAMBDA_FUNC_TYPES = (ast.FunctionDef, ast.AsyncFunctionDef) -NonLambdaFuncTypes_T = Union[ast.FunctionDef, ast.AsyncFunctionDef] def _fix_yield(i: int, tokens: List[Token]) -> None: @@ -46,14 +44,6 @@ def _is_simple_base(base: ast.AST) -> bool: ) -def _is_staticmethod_decorated(node: NonLambdaFuncTypes_T) -> bool: - for decorator in node.decorator_list: - if isinstance(decorator, ast.Name) and decorator.id == 'staticmethod': - return True - else: - return False - - class Scope: def __init__(self, node: ast.AST) -> None: self.node = node @@ -137,7 +127,6 @@ def visit_Call(self, node: ast.Call) -> None: isinstance(self._scopes[-1].node, NON_LAMBDA_FUNC_TYPES) and node.func.attr == self._scopes[-1].node.name and node.func.attr != '__new__' and - not _is_staticmethod_decorated(self._scopes[-1].node) and len(self._scopes[-1].node.args.args) >= 1 and node.args[0].id == self._scopes[-1].node.args.args[0].arg and # the function is an attribute of the contained class name diff --git a/tests/features/super_test.py b/tests/features/super_test.py index e58ffeab..8e3be14f 100644 --- a/tests/features/super_test.py +++ b/tests/features/super_test.py @@ -188,13 +188,6 @@ def test_fix_super(s, expected): ' return tuple.__new__(cls, (arg,))\n', id='super() does not work properly for __new__', ), - pytest.param( - 'class C(B):\n' - ' @staticmethod\n' - ' def f(arg):\n' - ' return B.f(arg)\n', - id='skip staticmethod', - ), ), ) def test_old_style_class_super_noop(s): @@ -214,17 +207,6 @@ def test_old_style_class_super_noop(s): ' super().f()\n' ' super().f(arg, arg)\n', ), - pytest.param( - 'class C(B):\n' - ' @classmethod\n' - ' def f(cls):\n' - ' B.f(cls)\n', - 'class C(B):\n' - ' @classmethod\n' - ' def f(cls):\n' - ' super().f()\n', - id='@classmethod', - ), pytest.param( 'class C(B):\n' ' def f(self, a):\n' From c67be292a34e3042c1c0a6711e1d643e35710719 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 28 Sep 2021 08:03:02 -0700 Subject: [PATCH 32/54] Revert "Merge pull request #320 from asottile/new_class_super_v2" This reverts commit b3f8c7b2e9ccb06c6028d5dd90c8f5490a49e95b, reversing changes made to 41308214566af7c5589ebc7e8a760522c82dc3bc. --- pyupgrade/_ast_helpers.py | 44 ------------- pyupgrade/_plugins/legacy.py | 82 ++++++++++++----------- tests/ast_helpers_test.py | 19 ------ tests/features/super_test.py | 104 ------------------------------ tests/features/yield_from_test.py | 19 ++++++ 5 files changed, 62 insertions(+), 206 deletions(-) delete mode 100644 tests/ast_helpers_test.py diff --git a/pyupgrade/_ast_helpers.py b/pyupgrade/_ast_helpers.py index 2f5c05ae..eb4ba066 100644 --- a/pyupgrade/_ast_helpers.py +++ b/pyupgrade/_ast_helpers.py @@ -1,12 +1,8 @@ import ast import warnings -from typing import Any from typing import Container from typing import Dict -from typing import Iterable from typing import Set -from typing import Tuple -from typing import Type from typing import Union from tokenize_rt import Offset @@ -61,43 +57,3 @@ def is_async_listcomp(node: ast.ListComp) -> bool: any(gen.is_async for gen in node.generators) or contains_await(node) ) - - -def _all_isinstance( - vals: Iterable[Any], - tp: Union[Type[Any], Tuple[Type[Any], ...]], -) -> bool: - return all(isinstance(v, tp) for v in vals) - - -def _fields_same(n1: ast.AST, n2: ast.AST) -> bool: - for (a1, v1), (a2, v2) in zip(ast.iter_fields(n1), ast.iter_fields(n2)): - # ignore ast attributes, they'll be covered by walk - if a1 != a2: - return False - elif _all_isinstance((v1, v2), ast.AST): - continue - elif _all_isinstance((v1, v2), (list, tuple)): - if len(v1) != len(v2): - return False - # ignore sequences which are all-ast, they'll be covered by walk - elif _all_isinstance(v1, ast.AST) and _all_isinstance(v2, ast.AST): - continue - elif v1 != v2: - return False - elif v1 != v2: - return False - return True - - -def targets_same(node1: ast.AST, node2: ast.AST) -> bool: - for t1, t2 in zip(ast.walk(node1), ast.walk(node2)): - # ignore `ast.Load` / `ast.Store` - if _all_isinstance((t1, t2), ast.expr_context): - continue - elif type(t1) != type(t2): - return False - elif not _fields_same(t1, t2): - return False - else: - return True diff --git a/pyupgrade/_plugins/legacy.py b/pyupgrade/_plugins/legacy.py index f7ba3763..321ffba0 100644 --- a/pyupgrade/_plugins/legacy.py +++ b/pyupgrade/_plugins/legacy.py @@ -2,19 +2,21 @@ import collections import contextlib import functools +from typing import Any from typing import Dict from typing import Generator from typing import Iterable from typing import List from typing import Set from typing import Tuple +from typing import Type +from typing import Union from tokenize_rt import Offset from tokenize_rt import Token from tokenize_rt import tokens_to_src from pyupgrade._ast_helpers import ast_to_offset -from pyupgrade._ast_helpers import targets_same from pyupgrade._data import register from pyupgrade._data import State from pyupgrade._data import TokenFunc @@ -24,7 +26,6 @@ from pyupgrade._token_helpers import find_token FUNC_TYPES = (ast.Lambda, ast.FunctionDef, ast.AsyncFunctionDef) -NON_LAMBDA_FUNC_TYPES = (ast.FunctionDef, ast.AsyncFunctionDef) def _fix_yield(i: int, tokens: List[Token]) -> None: @@ -35,13 +36,44 @@ def _fix_yield(i: int, tokens: List[Token]) -> None: tokens[i:block.end] = [Token('CODE', f'yield from {container}\n')] -def _is_simple_base(base: ast.AST) -> bool: - return ( - isinstance(base, ast.Name) or ( - isinstance(base, ast.Attribute) and - _is_simple_base(base.value) - ) - ) +def _all_isinstance( + vals: Iterable[Any], + tp: Union[Type[Any], Tuple[Type[Any], ...]], +) -> bool: + return all(isinstance(v, tp) for v in vals) + + +def _fields_same(n1: ast.AST, n2: ast.AST) -> bool: + for (a1, v1), (a2, v2) in zip(ast.iter_fields(n1), ast.iter_fields(n2)): + # ignore ast attributes, they'll be covered by walk + if a1 != a2: + return False + elif _all_isinstance((v1, v2), ast.AST): + continue + elif _all_isinstance((v1, v2), (list, tuple)): + if len(v1) != len(v2): + return False + # ignore sequences which are all-ast, they'll be covered by walk + elif _all_isinstance(v1, ast.AST) and _all_isinstance(v2, ast.AST): + continue + elif v1 != v2: + return False + elif v1 != v2: + return False + return True + + +def _targets_same(target: ast.AST, yield_value: ast.AST) -> bool: + for t1, t2 in zip(ast.walk(target), ast.walk(yield_value)): + # ignore `ast.Load` / `ast.Store` + if _all_isinstance((t1, t2), ast.expr_context): + continue + elif type(t1) != type(t2): + return False + elif not _fields_same(t1, t2): + return False + else: + return True class Scope: @@ -60,7 +92,6 @@ class Visitor(ast.NodeVisitor): def __init__(self) -> None: self._scopes: List[Scope] = [] self.super_offsets: Set[Offset] = set() - self.old_super_offsets: Set[Tuple[Offset, str]] = set() self.yield_offsets: Set[Offset] = set() @contextlib.contextmanager @@ -106,6 +137,7 @@ def visit_Call(self, node: ast.Call) -> None: len(node.args) == 2 and isinstance(node.args[0], ast.Name) and isinstance(node.args[1], ast.Name) and + # there are at least two scopes len(self._scopes) >= 2 and # the second to last scope is the class in arg1 isinstance(self._scopes[-2].node, ast.ClassDef) and @@ -116,29 +148,6 @@ def visit_Call(self, node: ast.Call) -> None: node.args[1].id == self._scopes[-1].node.args.args[0].arg ): self.super_offsets.add(ast_to_offset(node)) - elif ( - # base.funcname(funcarg1, ...) - isinstance(node.func, ast.Attribute) and - len(node.args) >= 1 and - isinstance(node.args[0], ast.Name) and - len(self._scopes) >= 2 and - # last stack is a function whose first argument is the first - # argument of this function - isinstance(self._scopes[-1].node, NON_LAMBDA_FUNC_TYPES) and - node.func.attr == self._scopes[-1].node.name and - node.func.attr != '__new__' and - len(self._scopes[-1].node.args.args) >= 1 and - node.args[0].id == self._scopes[-1].node.args.args[0].arg and - # the function is an attribute of the contained class name - isinstance(self._scopes[-2].node, ast.ClassDef) and - len(self._scopes[-2].node.bases) == 1 and - _is_simple_base(self._scopes[-2].node.bases[0]) and - targets_same( - self._scopes[-2].node.bases[0], - node.func.value, - ) - ): - self.old_super_offsets.add((ast_to_offset(node), node.func.attr)) self.generic_visit(node) @@ -150,7 +159,7 @@ def visit_For(self, node: ast.For) -> None: isinstance(node.body[0], ast.Expr) and isinstance(node.body[0].value, ast.Yield) and node.body[0].value.value is not None and - targets_same(node.target, node.body[0].value.value) and + _targets_same(node.target, node.body[0].value.value) and not node.orelse ): offset = ast_to_offset(node) @@ -189,10 +198,5 @@ def visit_Module( for offset in visitor.super_offsets: yield offset, super_func - for offset, func_name in visitor.old_super_offsets: - template = f'super().{func_name}({{rest}})' - callback = functools.partial(find_and_replace_call, template=template) - yield offset, callback - for offset in visitor.yield_offsets: yield offset, _fix_yield diff --git a/tests/ast_helpers_test.py b/tests/ast_helpers_test.py deleted file mode 100644 index 282ea729..00000000 --- a/tests/ast_helpers_test.py +++ /dev/null @@ -1,19 +0,0 @@ -import ast - -from pyupgrade._ast_helpers import _fields_same -from pyupgrade._ast_helpers import targets_same - - -def test_targets_same(): - assert targets_same(ast.parse('global a, b'), ast.parse('global a, b')) - assert not targets_same(ast.parse('global a'), ast.parse('global b')) - - -def _get_body(expr): - body = ast.parse(expr).body[0] - assert isinstance(body, ast.Expr) - return body.value - - -def test_fields_same(): - assert not _fields_same(_get_body('x'), _get_body('1')) diff --git a/tests/features/super_test.py b/tests/features/super_test.py index 8e3be14f..ee558a6d 100644 --- a/tests/features/super_test.py +++ b/tests/features/super_test.py @@ -122,107 +122,3 @@ def test_fix_super_noop(s): ) def test_fix_super(s, expected): assert _fix_plugins(s, settings=Settings(min_version=(3,))) == expected - - -@pytest.mark.parametrize( - 's', - ( - pytest.param( - 'class C(B):\n' - ' def f(self):\n' - ' B.f(notself)\n', - id='old style super, first argument is not first function arg', - ), - pytest.param( - 'class C(B1, B2):\n' - ' def f(self):\n' - ' B1.f(self)\n', - # TODO: is this safe to rewrite? I don't think so - id='old-style super, multiple inheritance first class', - ), - pytest.param( - 'class C(B1, B2):\n' - ' def f(self):\n' - ' B2.f(self)\n', - # TODO: is this safe to rewrite? I don't think so - id='old-style super, multiple inheritance not-first class', - ), - pytest.param( - 'class C(Base):\n' - ' def f(self):\n' - ' return [Base.f(self) for _ in ()]\n', - id='super in comprehension', - ), - pytest.param( - 'class C(Base):\n' - ' def f(self):\n' - ' def g():\n' - ' Base.f(self)\n' - ' g()\n', - id='super in nested functions', - ), - pytest.param( - 'class C(not_simple()):\n' - ' def f(self):\n' - ' not_simple().f(self)\n', - id='not a simple base', - ), - pytest.param( - 'class C(a().b):\n' - ' def f(self):\n' - ' a().b.f(self)\n', - id='non simple attribute base', - ), - pytest.param( - 'class C:\n' - ' @classmethod\n' - ' def make(cls, instance):\n' - ' ...\n' - 'class D(C):\n' - ' def find(self):\n' - ' return C.make(self)\n', - ), - pytest.param( - 'class C(tuple):\n' - ' def __new__(cls, arg):\n' - ' return tuple.__new__(cls, (arg,))\n', - id='super() does not work properly for __new__', - ), - ), -) -def test_old_style_class_super_noop(s): - assert _fix_plugins(s, settings=Settings(min_version=(3,))) == s - - -@pytest.mark.parametrize( - ('s', 'expected'), - ( - ( - 'class C(B):\n' - ' def f(self):\n' - ' B.f(self)\n' - ' B.f(self, arg, arg)\n', - 'class C(B):\n' - ' def f(self):\n' - ' super().f()\n' - ' super().f(arg, arg)\n', - ), - pytest.param( - 'class C(B):\n' - ' def f(self, a):\n' - ' B.f(\n' - ' self,\n' - ' a,\n' - ' )\n', - - 'class C(B):\n' - ' def f(self, a):\n' - ' super().f(\n' - ' a,\n' - ' )\n', - id='multi-line super call', - ), - ), -) -def test_old_style_class_super(s, expected): - assert _fix_plugins(s, settings=Settings(min_version=(3,))) == expected diff --git a/tests/features/yield_from_test.py b/tests/features/yield_from_test.py index dc040993..d3d12326 100644 --- a/tests/features/yield_from_test.py +++ b/tests/features/yield_from_test.py @@ -1,7 +1,11 @@ +import ast + import pytest from pyupgrade._data import Settings from pyupgrade._main import _fix_plugins +from pyupgrade._plugins.legacy import _fields_same +from pyupgrade._plugins.legacy import _targets_same @pytest.mark.parametrize( @@ -211,3 +215,18 @@ def test_fix_yield_from(s, expected): ) def test_fix_yield_from_noop(s): assert _fix_plugins(s, settings=Settings(min_version=(3,))) == s + + +def test_targets_same(): + assert _targets_same(ast.parse('global a, b'), ast.parse('global a, b')) + assert not _targets_same(ast.parse('global a'), ast.parse('global b')) + + +def _get_body(expr): + body = ast.parse(expr).body[0] + assert isinstance(body, ast.Expr) + return body.value + + +def test_fields_same(): + assert not _fields_same(_get_body('x'), _get_body('1')) From b6e103248fd97b04d2bea898b95d1d8776fece57 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 28 Sep 2021 08:15:18 -0700 Subject: [PATCH 33/54] v2.29.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 8397a38c..c2aa6537 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v2.28.1 + rev: v2.29.0 hooks: - id: pyupgrade args: [--py36-plus] diff --git a/README.md b/README.md index e01d38f2..e8ebc429 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Sample `.pre-commit-config.yaml`: ```yaml - repo: https://github.com/asottile/pyupgrade - rev: v2.28.1 + rev: v2.29.0 hooks: - id: pyupgrade ``` diff --git a/setup.cfg b/setup.cfg index 4993d089..b025f74b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pyupgrade -version = 2.28.1 +version = 2.29.0 description = A tool to automatically upgrade syntax for newer versions. long_description = file: README.md long_description_content_type = text/markdown From 6442ac67237ac97a13b89bfd1fe85aa5951ff33e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 11 Oct 2021 20:22:40 +0000 Subject: [PATCH 34/54] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/setup-cfg-fmt: v1.17.0 → v1.18.0](https://github.com/asottile/setup-cfg-fmt/compare/v1.17.0...v1.18.0) - [github.com/PyCQA/flake8: 3.9.2 → 4.0.1](https://github.com/PyCQA/flake8/compare/3.9.2...4.0.1) - [github.com/pre-commit/mirrors-mypy: v0.910 → v0.910-1](https://github.com/pre-commit/mirrors-mypy/compare/v0.910...v0.910-1) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c2aa6537..ff953a5e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,11 +11,11 @@ repos: - id: requirements-txt-fixer - id: trailing-whitespace - repo: https://github.com/asottile/setup-cfg-fmt - rev: v1.17.0 + rev: v1.18.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/PyCQA/flake8 - rev: 3.9.2 + rev: 4.0.1 hooks: - id: flake8 additional_dependencies: [flake8-typing-imports==1.7.0] @@ -39,6 +39,6 @@ repos: - id: pyupgrade args: [--py36-plus] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.910 + rev: v0.910-1 hooks: - id: mypy From 928508c07eac289641c005b5899a1c9e0df8b695 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 11 Oct 2021 20:26:19 +0000 Subject: [PATCH 35/54] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index b025f74b..ec2a611f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -17,6 +17,7 @@ classifiers = Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy From 50e3902ff4ecf1b3bd15a8516e6bb00211b16a6c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 23 Oct 2021 13:23:12 -0400 Subject: [PATCH 36/54] replace exit(main()) with raise SystemExit(main()) Committed via https://github.com/asottile/all-repos --- pyupgrade/__main__.py | 2 +- pyupgrade/_main.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyupgrade/__main__.py b/pyupgrade/__main__.py index 93592ffa..593a0b9f 100644 --- a/pyupgrade/__main__.py +++ b/pyupgrade/__main__.py @@ -1,4 +1,4 @@ from pyupgrade._main import main if __name__ == '__main__': - exit(main()) + raise SystemExit(main()) diff --git a/pyupgrade/_main.py b/pyupgrade/_main.py index 2675619e..646cab33 100644 --- a/pyupgrade/_main.py +++ b/pyupgrade/_main.py @@ -948,4 +948,4 @@ def main(argv: Optional[Sequence[str]] = None) -> int: if __name__ == '__main__': - exit(main()) + raise SystemExit(main()) From a7c2b0806050230640a0bcb30269607b6a4262d7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 25 Oct 2021 20:23:03 +0000 Subject: [PATCH 37/54] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/add-trailing-comma: v2.1.0 → v2.2.0](https://github.com/asottile/add-trailing-comma/compare/v2.1.0...v2.2.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ff953a5e..5f9b4572 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,7 +29,7 @@ repos: - id: reorder-python-imports args: [--py3-plus] - repo: https://github.com/asottile/add-trailing-comma - rev: v2.1.0 + rev: v2.2.0 hooks: - id: add-trailing-comma args: [--py36-plus] From 27584b84e05bc02fa62cb610c48f04bf2db791b2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Nov 2021 21:16:48 +0000 Subject: [PATCH 38/54] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/setup-cfg-fmt: v1.18.0 → v1.19.0](https://github.com/asottile/setup-cfg-fmt/compare/v1.18.0...v1.19.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5f9b4572..6060ca86 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ repos: - id: requirements-txt-fixer - id: trailing-whitespace - repo: https://github.com/asottile/setup-cfg-fmt - rev: v1.18.0 + rev: v1.19.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/PyCQA/flake8 From 886c3f0f21534314ee07486be5a05bda11491e7b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 15 Nov 2021 19:50:45 -0500 Subject: [PATCH 39/54] prevent rewriting union types with forward annotations --- pyupgrade/_plugins/typing_pep604.py | 20 ++++++++++++++++++++ tests/features/typing_pep604_test.py | 13 +++++++++++++ 2 files changed, 33 insertions(+) diff --git a/pyupgrade/_plugins/typing_pep604.py b/pyupgrade/_plugins/typing_pep604.py index 6ffcc205..8d550092 100644 --- a/pyupgrade/_plugins/typing_pep604.py +++ b/pyupgrade/_plugins/typing_pep604.py @@ -126,6 +126,15 @@ def _supported_version(state: State) -> bool: ) +def _any_arg_is_str(node_slice: ast.expr) -> bool: + return ( + isinstance(node_slice, ast.Str) or ( + isinstance(node_slice, ast.Tuple) and + any(isinstance(elt, ast.Str) for elt in node_slice.elts) + ) + ) + + @register(ast.Subscript) def visit_Subscript( state: State, @@ -135,6 +144,17 @@ def visit_Subscript( if not _supported_version(state): return + # prevent rewriting forward annotations + if ( + (sys.version_info >= (3, 9) and _any_arg_is_str(node.slice)) or + ( + sys.version_info < (3, 9) and + isinstance(node.slice, ast.Index) and + _any_arg_is_str(node.slice.value) + ) + ): + return + if is_name_attr(node.value, state.from_imports, 'typing', ('Optional',)): yield ast_to_offset(node), _fix_optional elif is_name_attr(node.value, state.from_imports, 'typing', ('Union',)): diff --git a/tests/features/typing_pep604_test.py b/tests/features/typing_pep604_test.py index 503bc2af..7b734e92 100644 --- a/tests/features/typing_pep604_test.py +++ b/tests/features/typing_pep604_test.py @@ -41,6 +41,19 @@ (3, 10), id='3.10+ empty Union', ), + # https://github.com/asottile/pyupgrade/issues/567 + pytest.param( + 'from typing import Optional\n' + 'def f() -> Optional["str"]: ...\n', + (3, 10), + id='3.10+ Optional of forward reference', + ), + pytest.param( + 'from typing import Union\n' + 'def f() -> Union[int, "str"]: ...\n', + (3, 10), + id='3.10+ Union of forward reference', + ), ), ) def test_fix_pep604_types_noop(s, version): From 9db80ebadc833c3863746007d3933f8f63aea759 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 15 Nov 2021 20:13:55 -0500 Subject: [PATCH 40/54] v2.29.1 --- .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 6060ca86..ea074838 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v2.29.0 + rev: v2.29.1 hooks: - id: pyupgrade args: [--py36-plus] diff --git a/README.md b/README.md index e8ebc429..c26d1af3 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Sample `.pre-commit-config.yaml`: ```yaml - repo: https://github.com/asottile/pyupgrade - rev: v2.29.0 + rev: v2.29.1 hooks: - id: pyupgrade ``` diff --git a/setup.cfg b/setup.cfg index ec2a611f..0c9782ae 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pyupgrade -version = 2.29.0 +version = 2.29.1 description = A tool to automatically upgrade syntax for newer versions. long_description = file: README.md long_description_content_type = text/markdown From c18e846a03303239b6d891e0debc1dcdae150c53 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Wed, 10 Nov 2021 16:35:47 +0000 Subject: [PATCH 41/54] Rewrite docs examples with commented code to use diffs --- README.md | 450 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 284 insertions(+), 166 deletions(-) diff --git a/README.md b/README.md index c26d1af3..9bbf3e97 100644 --- a/README.md +++ b/README.md @@ -29,38 +29,53 @@ Sample `.pre-commit-config.yaml`: ### Set literals -```python -set(()) # set() -set([]) # set() -set((1,)) # {1} -set((1, 2)) # {1, 2} -set([1, 2]) # {1, 2} -set(x for x in y) # {x for x in y} -set([x for x in y]) # {x for x in y} +```diff +-set(()) ++set() +-set([]) ++set() +-set((1,)) ++{1} +-set((1, 2)) ++{1, 2} +-set([1, 2]) ++{1, 2} +-set(x for x in y) ++{x for x in y} +-set([x for x in y]) ++{x for x in y} ``` ### Dictionary comprehensions -```python -dict((a, b) for a, b in y) # {a: b for a, b in y} -dict([(a, b) for a, b in y]) # {a: b for a, b in y} +```diff +-dict((a, b) for a, b in y) ++{a: b for a, b in y} +-dict([(a, b) for a, b in y]) ++{a: b for a, b in y} ``` ### Generator expressions for some built-in functions (pep 289) -```python -min([i for i in range(3)]) # min(i for i in range(3)) -max([i for i in range(3)]) # max(i for i in range(3)) -sum([i for i in range(3)]) # sum(i for i in range(3)) -''.join([str(i) for i in range(3)]) # ''.join(str(i) for i in range(3)) +```diff +-min([i for i in range(3)]) ++min(i for i in range(3)) +-max([i for i in range(3)]) ++max(i for i in range(3)) +-sum([i for i in range(3)]) ++sum(i for i in range(3)) +-''.join([str(i) for i in range(3)]) ++''.join(str(i) for i in range(3)) ``` ### Python2.7+ Format Specifiers -```python -'{0} {1}'.format(1, 2) # '{} {}'.format(1, 2) -'{0}' '{1}'.format(1, 2) # '{}' '{}'.format(1, 2) +```diff +-'{0} {1}'.format(1, 2) ++'{} {}'.format(1, 2) +-'{0}' '{1}'.format(1, 2) ++'{}' '{}'.format(1, 2) ``` ### printf-style string formatting @@ -68,10 +83,13 @@ sum([i for i in range(3)]) # sum(i for i in range(3)) Availability: - Unless `--keep-percent-format` is passed. -```python -'%s %s' % (a, b) # '{} {}'.format(a, b) -'%r %2f' % (a, b) # '{!r} {:2f}'.format(a, b) -'%(a)s %(b)s' % {'a': 1, 'b': 2} # '{a} {b}'.format(a=1, b=2) +```diff +-'%s %s' % (a, b) ++'{} {}'.format(a, b) +-'%r %2f' % (a, b) ++'{!r} {:2f}'.format(a, b) +-'%(a)s %(b)s' % {'a': 1, 'b': 2} ++'{a} {b}'.format(a=1, b=2) ``` ### Unicode literals @@ -80,24 +98,30 @@ Availability: - File imports `from __future__ import unicode_literals` - `--py3-plus` is passed on the commandline. -```python -u'foo' # 'foo' -u"foo" # 'foo' -u'''foo''' # '''foo''' +```diff +-u'foo' ++'foo' +-u"foo" ++'foo' +-u'''foo''' ++'''foo''' ``` ### Invalid escape sequences -```python -# strings with only invalid sequences become raw strings -'\d' # r'\d' -# strings with mixed valid / invalid sequences get escaped -'\n\d' # '\n\\d' -# `ur` is not a valid string prefix in python3 -u'\d' # u'\\d' - -# this fixes a syntax error in python3.3+ -'\N' # r'\N' +```diff + # strings with only invalid sequences become raw strings +-'\d' ++r'\d' + # strings with mixed valid / invalid sequences get escaped +-'\n\d' ++'\n\\d' + # `ur` is not a valid string prefix in python3 +-u'\d' ++u'\\d' + # this fixes a syntax error in python3.3+ +-'\N' ++r'\N' # note: pyupgrade is timid in one case (that's usually a mistake) # in python2.x `'\u2603'` is the same as `'\\u2603'` without `unicode_literals` @@ -109,58 +133,80 @@ u'\d' # u'\\d' In python3.8+, comparison to literals becomes a `SyntaxWarning` as the success of those comparisons is implementation specific (due to common object caching). -```python -x is 5 # x == 5 -x is not 5 # x != 5 -x is 'foo' # x == 'foo' +```diff +-x is 5 ++x == 5 +-x is not 5 ++x != 5 +-x is 'foo' ++x == 'foo' ``` ### `ur` string literals `ur'...'` literals are not valid in python 3.x -```python -ur'foo' # u'foo' -ur'\s' # u'\\s' -# unicode escapes are left alone -ur'\u2603' # u'\u2603' -ur'\U0001f643' # u'\U0001f643' +```diff +-ur'foo' ++u'foo' +-ur'\s' ++u'\\s' + # unicode escapes are left alone +-ur'\u2603' ++u'\u2603' +-ur'\U0001f643' ++u'\U0001f643' ``` ### `.encode()` to bytes literals -```python -'foo'.encode() # b'foo' -'foo'.encode('ascii') # b'foo' -'foo'.encode('utf-8') # b'foo' -u'foo'.encode() # b'foo' -'\xa0'.encode('latin1') # b'\xa0' +```diff +-'foo'.encode() ++b'foo' +-'foo'.encode('ascii') ++b'foo' +-'foo'.encode('utf-8') ++b'foo' +-u'foo'.encode() ++b'foo' +-'\xa0'.encode('latin1') ++b'\xa0' ``` ### Long literals -```python -5L # 5 -5l # 5 -123456789123456789123456789L # 123456789123456789123456789 +```diff +-5L ++5 +-5l ++5 +-123456789123456789123456789L ++123456789123456789123456789 ``` ### Octal literals -``` -0755 # 0o755 -05 # 5 +```diff +-0755 ++0o755 +-05 ++5 ``` ### extraneous parens in `print(...)` A fix for [python-modernize/python-modernize#178] -```python -print(()) # ok: printing an empty tuple -print((1,)) # ok: printing a tuple -sum((i for i in range(3)), []) # ok: parenthesized generator argument -print(("foo")) # print("foo") +```diff + # ok: printing an empty tuple + print(()) + # ok: printing a tuple + print((1,)) + # ok: parenthesized generator argument + sum((i for i in range(3)), []) + # fixed: +-print(("foo")) ++print("foo") ``` [python-modernize/python-modernize#178]: https://github.com/python-modernize/python-modernize/issues/178 @@ -189,10 +235,11 @@ Availability: Availability: - `--py3-plus` is passed on the commandline. -```python -class C(Base): - def f(self): - super(C, self).f() # super().f() +```diff + class C(Base): + def f(self): +- super(C, self).f() ++ super().f() ``` ### "new style" classes @@ -202,15 +249,18 @@ Availability: #### rewrites class declaration -```python -class C(object): pass # class C: pass -class C(B, object): pass # class C(B): pass +```diff +-class C(object): pass ++class C: pass +-class C(B, object): pass ++class C(B): pass ``` #### removes `__metaclass__ = type` declaration ```diff --__metaclass__ = type + class C: +- __metaclass__ = type ``` ### forced `str("native")` literals @@ -218,9 +268,11 @@ class C(B, object): pass # class C(B): pass Availability: - `--py3-plus` is passed on the commandline. -```python -str() # "''" -str("foo") # "foo" +```diff +-str() ++"''" +-str("foo") ++"foo" ``` ### `.encode("utf-8")` @@ -228,8 +280,9 @@ str("foo") # "foo" Availability: - `--py3-plus` is passed on the commandline. -```python -"foo".encode("utf-8") # "foo".encode() +```diff +-"foo".encode("utf-8") ++"foo".encode() ``` ### `# coding: ...` comment @@ -285,13 +338,14 @@ Availability: Availability: - `--py3-plus` is passed on the commandline. -```python -def f(): - for x in y: # yield from y - yield x - - for a, b in c: # yield from c - yield (a, b) +```diff + def f(): +- for x in y: +- yield x ++ yield from y +- for a, b in c: +- yield (a, b) ++ yield from c ``` ### Python2 and old Python3.x blocks @@ -351,74 +405,127 @@ Note that `if` blocks without an `else` will not be rewriten as it could introdu Availability: - `--py3-plus` is passed on the commandline. -```python -six.text_type # str -six.binary_type # bytes -six.class_types # (type,) -six.string_types # (str,) -six.integer_types # (int,) -six.unichr # chr -six.iterbytes # iter -six.print_(...) # print(...) -six.exec_(c, g, l) # exec(c, g, l) -six.advance_iterator(it) # next(it) -six.next(it) # next(it) -six.callable(x) # callable(x) -six.moves.range(x) # range(x) -six.moves.xrange(x) # range(x) - -from six import text_type -text_type # str - -@six.python_2_unicode_compatible # decorator is removed -class C: - def __str__(self): - return u'C()' - -class C(six.Iterator): pass # class C: pass - -class C(six.with_metaclass(M, B)): pass # class C(B, metaclass=M): pass - -@six.add_metaclass(M) # class C(B, metaclass=M): pass -class C(B): pass - -isinstance(..., six.class_types) # isinstance(..., type) -issubclass(..., six.integer_types) # issubclass(..., int) -isinstance(..., six.string_types) # isinstance(..., str) - -six.b('...') # b'...' -six.u('...') # '...' -six.byte2int(bs) # bs[0] -six.indexbytes(bs, i) # bs[i] -six.int2byte(i) # bytes((i,)) -six.iteritems(dct) # dct.items() -six.iterkeys(dct) # dct.keys() -six.itervalues(dct) # dct.values() -next(six.iteritems(dct)) # next(iter(dct.items())) -next(six.iterkeys(dct)) # next(iter(dct.keys())) -next(six.itervalues(dct)) # next(iter(dct.values())) -six.viewitems(dct) # dct.items() -six.viewkeys(dct) # dct.keys() -six.viewvalues(dct) # dct.values() -six.create_unbound_method(fn, cls) # fn -six.get_unbound_function(meth) # meth -six.get_method_function(meth) # meth.__func__ -six.get_method_self(meth) # meth.__self__ -six.get_function_closure(fn) # fn.__closure__ -six.get_function_code(fn) # fn.__code__ -six.get_function_defaults(fn) # fn.__defaults__ -six.get_function_globals(fn) # fn.__globals__ -six.raise_from(exc, exc_from) # raise exc from exc_from -six.reraise(tp, exc, tb) # raise exc.with_traceback(tb) -six.reraise(*sys.exc_info()) # raise -six.assertCountEqual(self, a1, a2) # self.assertCountEqual(a1, a2) -six.assertRaisesRegex(self, e, r, fn) # self.assertRaisesRegex(e, r, fn) -six.assertRegex(self, s, r) # self.assertRegex(s, r) - -# note: only for *literals* -six.ensure_binary('...') # b'...' -six.ensure_str('...') # '...' -six.ensure_text('...') # '...' +```diff +-six.text_type ++str +-six.binary_type ++bytes +-six.class_types ++(type,) +-six.string_types ++(str,) +-six.integer_types ++(int,) +-six.unichr ++chr +-six.iterbytes ++iter +-six.print_(...) ++print(...) +-six.exec_(c, g, l) ++exec(c, g, l) +-six.advance_iterator(it) ++next(it) +-six.next(it) ++next(it) +-six.callable(x) ++callable(x) +-six.moves.range(x) ++range(x) +-six.moves.xrange(x) ++range(x) + + +-from six import text_type +-text_type ++str + +-@six.python_2_unicode_compatible + class C: + def __str__(self): + return u'C()' + +-class C(six.Iterator): pass ++class C: pass + +-class C(six.with_metaclass(M, B)): pass ++class C(B, metaclass=M): pass + +-@six.add_metaclass(M) +-class C(B): pass ++class C(B, metaclass=M): pass + +-isinstance(..., six.class_types) ++isinstance(..., type) +-issubclass(..., six.integer_types) ++issubclass(..., int) +-isinstance(..., six.string_types) ++isinstance(..., str) + +-six.b('...') ++b'...' +-six.u('...') ++'...' +-six.byte2int(bs) ++bs[0] +-six.indexbytes(bs, i) ++bs[i] +-six.int2byte(i) ++bytes((i,)) +-six.iteritems(dct) ++dct.items() +-six.iterkeys(dct) ++dct.keys() +-six.itervalues(dct) ++dct.values() +-next(six.iteritems(dct)) ++next(iter(dct.items())) +-next(six.iterkeys(dct)) ++next(iter(dct.keys())) +-next(six.itervalues(dct)) ++next(iter(dct.values())) +-six.viewitems(dct) ++dct.items() +-six.viewkeys(dct) ++dct.keys() +-six.viewvalues(dct) ++dct.values() +-six.create_unbound_method(fn, cls) ++fn +-six.get_unbound_function(meth) ++meth +-six.get_method_function(meth) ++meth.__func__ +-six.get_method_self(meth) ++meth.__self__ +-six.get_function_closure(fn) ++fn.__closure__ +-six.get_function_code(fn) ++fn.__code__ +-six.get_function_defaults(fn) ++fn.__defaults__ +-six.get_function_globals(fn) ++fn.__globals__ +-six.raise_from(exc, exc_from) ++raise exc from exc_from +-six.reraise(tp, exc, tb) ++raise exc.with_traceback(tb) +-six.reraise(*sys.exc_info()) ++raise +-six.assertCountEqual(self, a1, a2) ++self.assertCountEqual(a1, a2) +-six.assertRaisesRegex(self, e, r, fn) ++self.assertRaisesRegex(e, r, fn) +-six.assertRegex(self, s, r) ++self.assertRegex(s, r) + + # note: only for *literals* +-six.ensure_binary('...') ++b'...' +-six.ensure_str('...') ++'...' +-six.ensure_text('...') ++'...' ``` ### `open` alias @@ -438,14 +545,21 @@ Availability: Availability: - `--py3-plus` is passed on the commandline. -```python -open("foo", "U") # open("foo") -open("foo", "Ur") # open("foo") -open("foo", "Ub") # open("foo", "rb") -open("foo", "rUb") # open("foo", "rb") -open("foo", "r") # open("foo") -open("foo", "rt") # open("foo") -open("f", "r", encoding="UTF-8") # open("f", encoding="UTF-8") +```diff +-open("foo", "U") ++open("foo") +-open("foo", "Ur") ++open("foo") +-open("foo", "Ub") ++open("foo", "rb") +-open("foo", "rUb") ++open("foo", "rb") +-open("foo", "r") ++open("foo") +-open("foo", "rt") ++open("foo") +-open("f", "r", encoding="UTF-8") ++open("f", encoding="UTF-8") ``` @@ -560,11 +674,15 @@ class D2(typing.TypedDict): Availability: - `--py36-plus` is passed on the commandline. -```python -'{foo} {bar}'.format(foo=foo, bar=bar) # f'{foo} {bar}' -'{} {}'.format(foo, bar) # f'{foo} {bar}' -'{} {}'.format(foo.bar, baz.womp) # f'{foo.bar} {baz.womp}' -'{} {}'.format(f(), g()) # f'{f()} {g()}' +```diff +-'{foo} {bar}'.format(foo=foo, bar=bar) ++f'{foo} {bar}' +-'{} {}'.format(foo, bar) ++f'{foo} {bar}' +-'{} {}'.format(foo.bar, baz.womp) ++f'{foo.bar} {baz.womp}' +-'{} {}'.format(f(), g()) ++f'{f()} {g()}' ``` _note_: `pyupgrade` is intentionally timid and will not create an f-string From 776492033f1a82db29ead52d44fe9773d75815e5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 22 Nov 2021 21:44:57 +0000 Subject: [PATCH 42/54] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/setup-cfg-fmt: v1.19.0 → v1.20.0](https://github.com/asottile/setup-cfg-fmt/compare/v1.19.0...v1.20.0) - [github.com/asottile/add-trailing-comma: v2.2.0 → v2.2.1](https://github.com/asottile/add-trailing-comma/compare/v2.2.0...v2.2.1) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ea074838..49f61a6a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ repos: - id: requirements-txt-fixer - id: trailing-whitespace - repo: https://github.com/asottile/setup-cfg-fmt - rev: v1.19.0 + rev: v1.20.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/PyCQA/flake8 @@ -29,7 +29,7 @@ repos: - id: reorder-python-imports args: [--py3-plus] - repo: https://github.com/asottile/add-trailing-comma - rev: v2.2.0 + rev: v2.2.1 hooks: - id: add-trailing-comma args: [--py36-plus] From 2afcc963f19d2c828dec20948f4583c48a6473b8 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 23 Nov 2021 11:20:50 -0500 Subject: [PATCH 43/54] Use org-default .github/FUNDING.yml Committed via https://github.com/asottile/all-repos --- .github/FUNDING.yml | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index eb54a96b..00000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1 +0,0 @@ -github: asottile From 6b580d5a25445f2dfd5c4bd8e5c7ac9ef146882f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 27 Nov 2021 21:04:12 -0800 Subject: [PATCH 44/54] improve coverage pragmas with covdefaults 2.1 --- pyupgrade/_main.py | 4 ++-- pyupgrade/_plugins/six_calls.py | 2 +- pyupgrade/_plugins/typing_pep563.py | 21 +++++++++------------ pyupgrade/_plugins/typing_pep604.py | 15 +++++++++------ pyupgrade/_string_helpers.py | 4 ++-- pyupgrade/_token_helpers.py | 8 ++++---- requirements-dev.txt | 2 +- tests/features/typing_pep563_test.py | 6 ++++++ tests/features/typing_pep604_test.py | 6 ++++++ 9 files changed, 40 insertions(+), 28 deletions(-) diff --git a/pyupgrade/_main.py b/pyupgrade/_main.py index 646cab33..5c92be4b 100644 --- a/pyupgrade/_main.py +++ b/pyupgrade/_main.py @@ -694,9 +694,9 @@ def _unparse(node: ast.expr) -> str: elif isinstance(node, ast.Attribute): return ''.join((_unparse(node.value), '.', node.attr)) elif isinstance(node, ast.Subscript): - if sys.version_info >= (3, 9): # pragma: no cover (py39+) + if sys.version_info >= (3, 9): # pragma: >=3.9 cover node_slice: ast.expr = node.slice - elif isinstance(node.slice, ast.Index): # pragma: no cover (= (3, 8): # pragma: no cover (py38+) +if sys.version_info >= (3, 8): # pragma: >=3.8 cover _EXPR_NEEDS_PARENS += (ast.NamedExpr,) SIX_CALLS = { diff --git a/pyupgrade/_plugins/typing_pep563.py b/pyupgrade/_plugins/typing_pep563.py index e16da37a..2cf7eba6 100644 --- a/pyupgrade/_plugins/typing_pep563.py +++ b/pyupgrade/_plugins/typing_pep563.py @@ -94,18 +94,15 @@ def _process_call(node: ast.Call) -> Iterable[ast.AST]: def _process_subscript(node: ast.Subscript) -> Iterable[ast.AST]: name = _get_name(node.value) if name == 'Annotated': - if sys.version_info >= (3, 9): # pragma: no cover (py39+) - node_slice: ast.expr = node.slice - elif isinstance(node.slice, ast.Index): # pragma: no cover (= (3, 9): # pragma: >=3.9 cover + node_slice = node.slice + elif isinstance(node.slice, ast.Index): # pragma: <3.9 cover + node_slice: ast.AST = node.slice.value + else: # pragma: <3.9 cover + node_slice = node.slice + + if isinstance(node_slice, ast.Tuple) and node_slice.elts: + yield node_slice.elts[0] elif name != 'Literal': yield node.slice diff --git a/pyupgrade/_plugins/typing_pep604.py b/pyupgrade/_plugins/typing_pep604.py index 8d550092..fdf803fd 100644 --- a/pyupgrade/_plugins/typing_pep604.py +++ b/pyupgrade/_plugins/typing_pep604.py @@ -158,12 +158,15 @@ def visit_Subscript( if is_name_attr(node.value, state.from_imports, 'typing', ('Optional',)): yield ast_to_offset(node), _fix_optional elif is_name_attr(node.value, state.from_imports, 'typing', ('Union',)): - if sys.version_info >= (3, 9): # pragma: no cover (py39+) - node_slice: ast.expr = node.slice - elif isinstance(node.slice, ast.Index): # pragma: no cover (= (3, 9): # pragma: >=3.9 cover + node_slice = node.slice + elif isinstance(node.slice, ast.Index): # pragma: <3.9 cover + node_slice: ast.AST = node.slice.value + else: # pragma: <3.9 cover + node_slice = node.slice # unexpected slice type + + if isinstance(node_slice, ast.Slice): # not a valid annotation + return if isinstance(node_slice, ast.Tuple): if node_slice.elts: diff --git a/pyupgrade/_string_helpers.py b/pyupgrade/_string_helpers.py index aac52cb0..42fb587b 100644 --- a/pyupgrade/_string_helpers.py +++ b/pyupgrade/_string_helpers.py @@ -3,9 +3,9 @@ import string import sys -if sys.version_info >= (3, 7): # pragma: no cover (py37+) +if sys.version_info >= (3, 7): # pragma: >=3.7 cover is_ascii = str.isascii -else: # pragma: no cover ( bool: return all(c in string.printable for c in s) diff --git a/pyupgrade/_token_helpers.py b/pyupgrade/_token_helpers.py index 44d9b575..a076a356 100644 --- a/pyupgrade/_token_helpers.py +++ b/pyupgrade/_token_helpers.py @@ -65,14 +65,14 @@ def find_end(tokens: List[Token], i: int) -> int: return i -if sys.version_info >= (3, 8): # pragma: no cover (py38+) +if sys.version_info >= (3, 8): # pragma: >=3.8 cover # python 3.8 fixed the offsets of generators / tuples def _arg_token_index(tokens: List[Token], i: int, arg: ast.expr) -> int: idx = _search_until(tokens, i, arg) + 1 while idx < len(tokens) and tokens[idx].name in NON_CODING_TOKENS: idx += 1 return idx -else: # pragma: no cover ( int: # lists containing non-tuples report the first element correctly if isinstance(arg, ast.List): @@ -502,12 +502,12 @@ def replace_argument( def find_comprehension_opening_bracket(i: int, tokens: List[Token]) -> int: """Find opening bracket of comprehension given first argument.""" - if sys.version_info < (3, 8): # pragma: no cover (py38+) + if sys.version_info < (3, 8): # pragma: <3.8 cover i -= 1 while not (tokens[i].name == 'OP' and tokens[i].src == '[') and i: i -= 1 return i - else: # pragma: no cover (=3.8 cover return i diff --git a/requirements-dev.txt b/requirements-dev.txt index 0c5a37eb..2698f6cf 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,3 +1,3 @@ -covdefaults +covdefaults>=2.1.0 coverage pytest diff --git a/tests/features/typing_pep563_test.py b/tests/features/typing_pep563_test.py index e7b82d93..f2ee80c8 100644 --- a/tests/features/typing_pep563_test.py +++ b/tests/features/typing_pep563_test.py @@ -57,6 +57,12 @@ (3,), id='Kwonly, untyped', ), + pytest.param( + 'from __future__ import annotations\n' + 'x: Annotated[1:2] = ...\n', + (3,), + id='Annotated with invalid slice', + ), ), ) def test_fix_typing_pep563_noop(s, version): diff --git a/tests/features/typing_pep604_test.py b/tests/features/typing_pep604_test.py index 7b734e92..d27c7951 100644 --- a/tests/features/typing_pep604_test.py +++ b/tests/features/typing_pep604_test.py @@ -54,6 +54,12 @@ (3, 10), id='3.10+ Union of forward reference', ), + pytest.param( + 'from typing import Union\n' + 'def f() -> Union[1:2]: ...\n', + (3, 10), + id='invalid Union slicing', + ), ), ) def test_fix_pep604_types_noop(s, version): From 4cc80a6e5cd8df681123910d7d4e2a9a5a7b3e94 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 20 Dec 2021 21:40:07 +0000 Subject: [PATCH 45/54] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v0.910-1 → v0.920](https://github.com/pre-commit/mirrors-mypy/compare/v0.910-1...v0.920) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 49f61a6a..7d5975bb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -39,6 +39,6 @@ repos: - id: pyupgrade args: [--py36-plus] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.910-1 + rev: v0.920 hooks: - id: mypy From b939e709a9d70ed7b480716480bfe3e4a84f3cb2 Mon Sep 17 00:00:00 2001 From: Ali Date: Sun, 26 Dec 2021 21:41:37 +0100 Subject: [PATCH 46/54] fix the diff output of `forced str("native") literals` section --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9bbf3e97..5fa9cbf1 100644 --- a/README.md +++ b/README.md @@ -270,7 +270,7 @@ Availability: ```diff -str() -+"''" ++'' -str("foo") +"foo" ``` From f797d5a284cf59a395d8398229a7731834dd9ba8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 27 Dec 2021 22:21:08 +0000 Subject: [PATCH 47/54] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/pre-commit-hooks: v4.0.1 → v4.1.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.0.1...v4.1.0) - [github.com/pre-commit/mirrors-mypy: v0.920 → v0.930](https://github.com/pre-commit/mirrors-mypy/compare/v0.920...v0.930) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7d5975bb..c4d0384e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.0.1 + rev: v4.1.0 hooks: - id: check-docstring-first - id: check-yaml @@ -39,6 +39,6 @@ repos: - id: pyupgrade args: [--py36-plus] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.920 + rev: v0.930 hooks: - id: mypy From a9485aa8672166e8aebb58c008baac09b4ee1d4f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 27 Dec 2021 19:06:40 -0500 Subject: [PATCH 48/54] fix __path__ type annotation --- pyupgrade/_data.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyupgrade/_data.py b/pyupgrade/_data.py index e82fa464..5d7aaf7b 100644 --- a/pyupgrade/_data.py +++ b/pyupgrade/_data.py @@ -116,8 +116,7 @@ def visit( def _import_plugins() -> None: - # https://github.com/python/mypy/issues/1422 - plugins_path: str = _plugins.__path__ # type: ignore + plugins_path = _plugins.__path__ mod_infos = pkgutil.walk_packages(plugins_path, f'{_plugins.__name__}.') for _, name, _ in mod_infos: __import__(name, fromlist=['_trash']) From 1229946ba9336fbe1c5db19be85076a617feb23a Mon Sep 17 00:00:00 2001 From: MarcoGorelli Date: Mon, 27 Dec 2021 15:00:33 +0000 Subject: [PATCH 49/54] rewrite abspath(__file__) to __file__ in py39+ --- README.md | 13 +++++++ pyupgrade/_data.py | 1 + pyupgrade/_plugins/abspath_file.py | 53 +++++++++++++++++++++++++++++ tests/features/abspath_file_test.py | 52 ++++++++++++++++++++++++++++ 4 files changed, 119 insertions(+) create mode 100644 pyupgrade/_plugins/abspath_file.py create mode 100644 tests/features/abspath_file_test.py diff --git a/README.md b/README.md index 9bbf3e97..9352083c 100644 --- a/README.md +++ b/README.md @@ -756,6 +756,19 @@ Availability: ``` +### remove unnecessary abspath + +Availability: +- `--py39-plus` is passed on the commandline. + +```diff + from os.path import abspath + +-abspath(__file__) ++__file__ +``` + + ### pep 604 typing rewrites Availability: diff --git a/pyupgrade/_data.py b/pyupgrade/_data.py index e82fa464..1581a559 100644 --- a/pyupgrade/_data.py +++ b/pyupgrade/_data.py @@ -44,6 +44,7 @@ class State(NamedTuple): RECORD_FROM_IMPORTS = frozenset(( '__future__', + 'os.path', 'functools', 'mmap', 'select', diff --git a/pyupgrade/_plugins/abspath_file.py b/pyupgrade/_plugins/abspath_file.py new file mode 100644 index 00000000..b70b2dbf --- /dev/null +++ b/pyupgrade/_plugins/abspath_file.py @@ -0,0 +1,53 @@ +import ast +from typing import Iterable +from typing import List +from typing import Tuple + +from tokenize_rt import Offset +from tokenize_rt import Token + +from pyupgrade._ast_helpers import ast_to_offset +from pyupgrade._data import register +from pyupgrade._data import State +from pyupgrade._data import TokenFunc +from pyupgrade._token_helpers import find_closing_bracket +from pyupgrade._token_helpers import find_open_paren + + +def _remove_abspath(i: int, tokens: List[Token]) -> None: + paren_start = find_open_paren(tokens, i + 1) + paren_end = find_closing_bracket(tokens, paren_start) + while i <= paren_start: + tokens[i] = Token('PLACEHOLDER', '') + i += 1 + tokens[paren_end] = Token('PLACEHOLDER', '') + + +@register(ast.Call) +def visit_Call( + state: State, + node: ast.Call, + parent: ast.AST, +) -> Iterable[Tuple[Offset, TokenFunc]]: + if ( + state.settings.min_version >= (3, 9) and + ( + ( + isinstance(node.func, ast.Name) and + node.func.id == 'abspath' and + node.func.id in state.from_imports['os.path'] + ) or + ( + isinstance(node.func, ast.Attribute) and + isinstance(node.func.value, ast.Attribute) and + isinstance(node.func.value.value, ast.Name) and + node.func.value.value.id == 'os' and + node.func.value.attr == 'path' and + node.func.attr == 'abspath' + ) + ) and + len(node.args) == 1 and + isinstance(node.args[0], ast.Name) and + node.args[0].id == '__file__' + ): + yield ast_to_offset(node), _remove_abspath diff --git a/tests/features/abspath_file_test.py b/tests/features/abspath_file_test.py new file mode 100644 index 00000000..dabbaa2a --- /dev/null +++ b/tests/features/abspath_file_test.py @@ -0,0 +1,52 @@ +import pytest + +from pyupgrade._data import Settings +from pyupgrade._main import _fix_plugins + + +@pytest.mark.parametrize( + ('s', 'expected'), + ( + pytest.param( + 'from os.path import abspath\n' + 'abspath(__file__)', + 'from os.path import abspath\n' + '__file__', + id='abspath', + ), + pytest.param( + 'import os\n' + 'os.path.abspath(__file__)', + 'import os\n' + '__file__', + id='os.path.abspath', + ), + ), +) +def test_fix_abspath_file(s, expected): + ret = _fix_plugins(s, settings=Settings(min_version=(3, 9))) + assert ret == expected + + +@pytest.mark.parametrize( + 's, min_version', + ( + pytest.param( + 'abspath(__file__)', + (3, 8), + id='Not Python3.9+', + ), + pytest.param( + 'os.path.abspath(file)', + (3, 9), + id='Abspath of not-__file__', + ), + pytest.param( + 'os.path.abspath(file, foo)', + (3, 9), + id='Garbage (don\'t rewrite)', + ), + ), +) +def test_fix_abspath_file_noop(s, min_version): + assert _fix_plugins(s, settings=Settings(min_version=min_version)) == s From 234e07160f1d6a171b1100de7046a92d4866e0ba Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 27 Dec 2021 19:49:26 -0500 Subject: [PATCH 50/54] v2.30.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 c4d0384e..ec8aab01 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v2.29.1 + rev: v2.30.0 hooks: - id: pyupgrade args: [--py36-plus] diff --git a/README.md b/README.md index 076406ee..af31768b 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Sample `.pre-commit-config.yaml`: ```yaml - repo: https://github.com/asottile/pyupgrade - rev: v2.29.1 + rev: v2.30.0 hooks: - id: pyupgrade ``` diff --git a/setup.cfg b/setup.cfg index 0c9782ae..5e1f47a2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pyupgrade -version = 2.29.1 +version = 2.30.0 description = A tool to automatically upgrade syntax for newer versions. long_description = file: README.md long_description_content_type = text/markdown From 46f6f773b3c36194a96fd0889ed5ad6ae0059aed Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 30 Dec 2021 08:35:35 -0800 Subject: [PATCH 51/54] don't rewrite six.reraise with named args --- pyupgrade/_plugins/six_calls.py | 13 +++++++++++-- tests/features/six_test.py | 1 + 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/pyupgrade/_plugins/six_calls.py b/pyupgrade/_plugins/six_calls.py index 90c4ee30..ac58fd57 100644 --- a/pyupgrade/_plugins/six_calls.py +++ b/pyupgrade/_plugins/six_calls.py @@ -172,13 +172,21 @@ def visit_Call( ('reraise',), ) ): - if len(node.args) == 2 and not has_starargs(node): + if ( + len(node.args) == 2 and + not node.keywords and + not has_starargs(node) + ): func = functools.partial( find_and_replace_call, template=RERAISE_2_TMPL, ) yield ast_to_offset(node), func - elif len(node.args) == 3 and not has_starargs(node): + elif ( + len(node.args) == 3 and + not node.keywords and + not has_starargs(node) + ): func = functools.partial( find_and_replace_call, template=RERAISE_3_TMPL, @@ -186,6 +194,7 @@ def visit_Call( yield ast_to_offset(node), func elif ( len(node.args) == 1 and + not node.keywords and isinstance(node.args[0], ast.Starred) and isinstance(node.args[0].value, ast.Call) and is_name_attr( diff --git a/tests/features/six_test.py b/tests/features/six_test.py index e1f6cd88..4ea3f5b5 100644 --- a/tests/features/six_test.py +++ b/tests/features/six_test.py @@ -24,6 +24,7 @@ 'class C(six.with_metaclass(Meta, B), D): pass', # cannot determine args to rewrite them 'six.reraise(*err)', 'six.u(*a)', + 'six.reraise(a, b, tb=c)', 'class C(six.with_metaclass(*a)): pass', '@six.add_metaclass(*a)\n' 'class C: pass\n', From 88fe1b5f43a286837ddb8cf65f55ea5b3dc463c7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 30 Dec 2021 08:43:34 -0800 Subject: [PATCH 52/54] v2.30.1 --- .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 ec8aab01..18c13de7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v2.30.0 + rev: v2.30.1 hooks: - id: pyupgrade args: [--py36-plus] diff --git a/README.md b/README.md index af31768b..2de1ba4a 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Sample `.pre-commit-config.yaml`: ```yaml - repo: https://github.com/asottile/pyupgrade - rev: v2.30.0 + rev: v2.30.1 hooks: - id: pyupgrade ``` diff --git a/setup.cfg b/setup.cfg index 5e1f47a2..0c2a3f6b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pyupgrade -version = 2.30.0 +version = 2.30.1 description = A tool to automatically upgrade syntax for newer versions. long_description = file: README.md long_description_content_type = text/markdown From 416deab5aa44e4a2aa2ba2c6cc0f8795df88c04c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 31 Dec 2021 16:35:00 -0800 Subject: [PATCH 53/54] rewrite string formatting with **locals() --- README.md | 2 ++ pyupgrade/_plugins/format_locals.py | 49 +++++++++++++++++++++++++ tests/features/format_locals_test.py | 53 ++++++++++++++++++++++++++++ tests/features/fstrings_test.py | 2 -- 4 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 pyupgrade/_plugins/format_locals.py create mode 100644 tests/features/format_locals_test.py diff --git a/README.md b/README.md index 2de1ba4a..3ed661f5 100644 --- a/README.md +++ b/README.md @@ -683,6 +683,8 @@ Availability: +f'{foo.bar} {baz.womp}' -'{} {}'.format(f(), g()) +f'{f()} {g()}' +-'{x}'.format(**locals()) ++f'{x}' ``` _note_: `pyupgrade` is intentionally timid and will not create an f-string diff --git a/pyupgrade/_plugins/format_locals.py b/pyupgrade/_plugins/format_locals.py new file mode 100644 index 00000000..8c736ec6 --- /dev/null +++ b/pyupgrade/_plugins/format_locals.py @@ -0,0 +1,49 @@ +import ast +from typing import Iterable +from typing import List +from typing import Tuple + +from tokenize_rt import Offset +from tokenize_rt import rfind_string_parts +from tokenize_rt import Token + +from pyupgrade._ast_helpers import ast_to_offset +from pyupgrade._data import register +from pyupgrade._data import State +from pyupgrade._data import TokenFunc +from pyupgrade._token_helpers import find_closing_bracket +from pyupgrade._token_helpers import find_open_paren +from pyupgrade._token_helpers import find_token + + +def _fix(i: int, tokens: List[Token]) -> None: + dot_pos = find_token(tokens, i, '.') + open_pos = find_open_paren(tokens, dot_pos) + close_pos = find_closing_bracket(tokens, open_pos) + for string_idx in rfind_string_parts(tokens, dot_pos - 1): + tok = tokens[string_idx] + tokens[string_idx] = tok._replace(src=f'f{tok.src}') + del tokens[dot_pos:close_pos + 1] + + +@register(ast.Call) +def visit_Call( + state: State, + node: ast.Call, + parent: ast.AST, +) -> Iterable[Tuple[Offset, TokenFunc]]: + if ( + state.settings.min_version >= (3, 6) and + isinstance(node.func, ast.Attribute) and + isinstance(node.func.value, ast.Str) and + node.func.attr == 'format' and + len(node.args) == 0 and + len(node.keywords) == 1 and + node.keywords[0].arg is None and + isinstance(node.keywords[0].value, ast.Call) and + isinstance(node.keywords[0].value.func, ast.Name) and + node.keywords[0].value.func.id == 'locals' and + len(node.keywords[0].value.args) == 0 and + len(node.keywords[0].value.keywords) == 0 + ): + yield ast_to_offset(node), _fix diff --git a/tests/features/format_locals_test.py b/tests/features/format_locals_test.py new file mode 100644 index 00000000..b761841b --- /dev/null +++ b/tests/features/format_locals_test.py @@ -0,0 +1,53 @@ +import pytest + +from pyupgrade._data import Settings +from pyupgrade._main import _fix_plugins + + +@pytest.mark.parametrize( + ('s', 'version'), + ( + pytest.param( + '"{x}".format(**locals())', + (3,), + id='not 3.6+', + ), + pytest.param( + '"{x} {y}".format(x, **locals())', + (3, 6), + id='mixed locals() and params', + ), + ), +) +def test_fix_format_locals_noop(s, version): + assert _fix_plugins(s, settings=Settings(min_version=version)) == s + + +@pytest.mark.parametrize( + ('s', 'expected'), + ( + pytest.param( + '"{x}".format(**locals())', + 'f"{x}"', + id='normal case', + ), + pytest.param( + '"{x}" "{y}".format(**locals())', + 'f"{x}" f"{y}"', + id='joined strings', + ), + pytest.param( + '(\n' + ' "{x}"\n' + ' "{y}"\n' + ').format(**locals())\n', + '(\n' + ' f"{x}"\n' + ' f"{y}"\n' + ')\n', + id='joined strings with parens', + ), + ), +) +def test_fix_format_locals(s, expected): + assert _fix_plugins(s, settings=Settings(min_version=(3, 6))) == expected diff --git a/tests/features/fstrings_test.py b/tests/features/fstrings_test.py index 61c2cc1c..425b84e2 100644 --- a/tests/features/fstrings_test.py +++ b/tests/features/fstrings_test.py @@ -61,8 +61,6 @@ def test_fix_fstrings_noop(s): r'f"\N{snowman} {a}"', id='named escape sequences', ), - # TODO: poor man's f-strings? - # '"{foo}".format(**locals())' ), ) def test_fix_fstrings(s, expected): From e695ecd365119ab4e5463f6e49bea5f4b7ca786b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 31 Dec 2021 16:43:09 -0800 Subject: [PATCH 54/54] v2.31.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 18c13de7..d3fcedbe 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v2.30.1 + rev: v2.31.0 hooks: - id: pyupgrade args: [--py36-plus] diff --git a/README.md b/README.md index 3ed661f5..b6f56ad9 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Sample `.pre-commit-config.yaml`: ```yaml - repo: https://github.com/asottile/pyupgrade - rev: v2.30.1 + rev: v2.31.0 hooks: - id: pyupgrade ``` diff --git a/setup.cfg b/setup.cfg index 0c2a3f6b..4253699b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pyupgrade -version = 2.30.1 +version = 2.31.0 description = A tool to automatically upgrade syntax for newer versions. long_description = file: README.md long_description_content_type = text/markdown