From 04e3d6d0074fbfa1fc8ccc1bde4aef5e36d608dc Mon Sep 17 00:00:00 2001 From: colin99d Date: Thu, 22 Dec 2022 22:54:41 -0500 Subject: [PATCH 01/15] Correctly parse one arg --- pyupgrade/_plugins/type_bases_object.py | 57 +++++++++ tests/features/type_bases_object_test.py | 144 +++++++++++++++++++++++ 2 files changed, 201 insertions(+) create mode 100644 pyupgrade/_plugins/type_bases_object.py create mode 100644 tests/features/type_bases_object_test.py diff --git a/pyupgrade/_plugins/type_bases_object.py b/pyupgrade/_plugins/type_bases_object.py new file mode 100644 index 00000000..709ede01 --- /dev/null +++ b/pyupgrade/_plugins/type_bases_object.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +import ast +import functools +from typing import Iterable + +from tokenize_rt import Offset +from tokenize_rt import Token +from tokenize_rt import UNIMPORTANT_WS + +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_open_paren +from pyupgrade._token_helpers import parse_call_args +from pyupgrade._token_helpers import delete_argument + + +def remove_base_class_from_type_call( + i: int, tokens: list[Token], *, count: int +) -> None: + token_list = [x.src for x in tokens] + print(token_list) + if count == 1: + if "object" in token_list: + idx = token_list.index("object") + add = 2 if token_list[idx + 1] == "," else 1 + del tokens[idx: idx + add] + elif count == 2: + idx = token_list.index("object") + add = 2 if token_list[idx + 1] == "," else 1 + del tokens[idx] + + +@register(ast.Call) +def visit_Call( + state: State, + node: ast.Call, + parent: ast.AST, +) -> Iterable[tuple[Offset, TokenFunc]]: + if ( + isinstance(node.func, ast.Name) + and node.func.id == "type" + and len(node.args) > 1 + and isinstance(node.args[1], ast.Tuple) + and any( + isinstance(elt, ast.Name) and elt.id == "object" + for elt in node.args[1].elts + ) + ): + for base in node.args[1].elts: + if isinstance(base, ast.Name) and base.id == "object": + func = functools.partial( + remove_base_class_from_type_call, count=len(node.args[1].elts) + ) + yield ast_to_offset(base), func diff --git a/tests/features/type_bases_object_test.py b/tests/features/type_bases_object_test.py new file mode 100644 index 00000000..e1ff0fad --- /dev/null +++ b/tests/features/type_bases_object_test.py @@ -0,0 +1,144 @@ +from __future__ import annotations + +import pytest + +from pyupgrade._data import Settings +from pyupgrade._main import _fix_plugins + + +@pytest.mark.parametrize( + 'src', + ['A = type("A", (), {})', 'B = type("B", (int,), {}'], +) +def test_fix_type_bases_object_noop(src): + ret = _fix_plugins(src, settings=Settings()) + assert ret == src +""" +pytest.param( + 'C = type("C", (object, foo, bar), {})', + 'C = type("C", (foo, bar), {})', + id='three base classes, object first', +), +pytest.param( + 'D = type("D", (tuple, object), {})', + 'D = type("D", (tuple,), {})', + id='two base classes, object last', +), +pytest.param( + 'E = type("E", (foo, bar, object), {})', + 'E = type("E", (foo, bar), {})', + id='three base classes, object last', +), +pytest.param( + 'F = type(\n "F",\n (object, tuple),\n {}\n)', + 'F = type(\n "F",\n (tuple,),\n {}\n)', + id='newline and indent, two base classes', +), +pytest.param( + 'G = type(\n "G",\n (\n object,\n class1,\n' + ' class2,\n class3,\n class4,\n class5' + ',\n class6,\n class7,\n class8,\n ' + 'class9,\n classA,\n classB\n ),\n {}\n)', + 'G = type(\n "G",\n (\n class1,\n class2,\n' + ' class3,\n class4,\n class5,\n class6' + ',\n class7,\n class8,\n class9,\n ' + 'classA,\n classB\n ),\n {}\n)', + id='newline and also inside classes tuple', +), +pytest.param( + 'H = type(\n "H",\n (tuple, object),\n {}\n)', + 'H = type(\n "H",\n (tuple,),\n {}\n)', + id='newline and indent, two base classes, object last', +), +pytest.param( + 'I = type(\n "I",\n (\n class1,\n' + ' class2,\n class3,\n class4,\n class5' + ',\n class6,\n class7,\n class8,\n ' + 'class9,\n classA,\n object\n ),\n {}\n)', + 'I = type(\n "I",\n (\n class1,\n class2,\n' + ' class3,\n class4,\n class5,\n class6' + ',\n class7,\n class8,\n class9,\n ' + 'classA\n ),\n {}\n)', + id='newline and also inside classes tuple, object last', +), +pytest.param( + 'J = type("J", (object, foo, bar,), {})', + 'J = type("J", (foo, bar,), {})', + id='trailing comma, object first', +), +pytest.param( + 'K = type("K", (foo, bar, object,), {})', + 'K = type("K", (foo, bar,), {})', + id='trailing comma, object last', +), +pytest.param( + 'L = type(\n "L",\n (foo, bar, object,),\n {}\n)', + 'L = type(\n "L",\n (foo, bar,),\n {}\n)', + id='trailing comma, newline and indent, object last', +), +pytest.param( + 'M = type(\n "M",\n (\n class1,\n' + ' class2,\n class3,\n class4,\n class5' + ',\n class6,\n class7,\n class8,\n ' + 'class9,\n classA,\n object,\n ),\n {}\n)', + 'M = type(\n "M",\n (\n class1,\n class2,\n' + ' class3,\n class4,\n class5,\n class6' + ',\n class7,\n class8,\n class9,\n ' + 'classA,\n ),\n {}\n)', + id='trailing comma, ' + 'newline and also inside classes tuple, ' + 'object last', +), +pytest.param( + 'O = type("O", (foo, object, bar), {})', + 'O = type("O", (foo, bar), {})', + id='object in the middle', +), +pytest.param( + 'P = type( \n"P",\n (\n foo,\n object,' + '\n bar\n ),\n {}\n)', + 'P = type( \n"P",\n (\n foo,\n bar\n ' + '),\n {}\n)', + id='newline and also inside classes tuple, object in the middle', +), +pytest.param( + 'Q = type(\n "Q",\n (foo, object, bar),\n {}\n)', + 'Q = type(\n "Q",\n (foo, bar),\n {}\n)', + id='newline and indent, object in the middle', +), +pytest.param( + 'R = type("R", (object,tuple), {})', + 'R = type("R", (tuple,), {})', + id='no spaces, object first', +), +pytest.param( + 'S = type("S", (tuple,object), {})', + 'S = type("S", (tuple,), {})', + id='no spaces, object last', +), +pytest.param( + 'U = type("U", (tuple, object,), {})', + 'U = type("U", (tuple,), {})', + id='trailing comma, object last, two classes', +), +""" + + +@pytest.mark.parametrize( + ('s', 'expected'), + ( + pytest.param( + 'A = type("A", (object,), {})', + 'A = type("A", (), {})', + id='only object base class', + ), + pytest.param( + 'B = type("B", (object, tuple), {})', + 'B = type("B", (tuple,), {})', + id='two base classes, object first', + ), + ), +) +def test_fix_type_bases_object(s, expected): + ret = _fix_plugins(s, settings=Settings()) + assert ret == expected From f201c9817387f6dbb6b423e8042624c26ede4e61 Mon Sep 17 00:00:00 2001 From: colin99d Date: Fri, 23 Dec 2022 08:09:01 -0500 Subject: [PATCH 02/15] Got partially working for 2 --- pyupgrade/_plugins/type_bases_object.py | 34 ++++++++++++++++--------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/pyupgrade/_plugins/type_bases_object.py b/pyupgrade/_plugins/type_bases_object.py index 709ede01..f6440c83 100644 --- a/pyupgrade/_plugins/type_bases_object.py +++ b/pyupgrade/_plugins/type_bases_object.py @@ -17,20 +17,28 @@ from pyupgrade._token_helpers import delete_argument +def remove_all(the_list: list[str], item: str) -> list[str]: + return [x for x in the_list if x != item] + + def remove_base_class_from_type_call( - i: int, tokens: list[Token], *, count: int + i: int, tokens: list[Token], *, node_objs_count: int ) -> None: token_list = [x.src for x in tokens] - print(token_list) - if count == 1: - if "object" in token_list: - idx = token_list.index("object") - add = 2 if token_list[idx + 1] == "," else 1 - del tokens[idx: idx + add] - elif count == 2: - idx = token_list.index("object") - add = 2 if token_list[idx + 1] == "," else 1 - del tokens[idx] + print(tokens) + type_start = find_open_paren(tokens, 0) + bases_start = find_open_paren(tokens, type_start + 1) + bases, end = parse_call_args(tokens, bases_start) + inner_tokens = token_list[bases_start + 1 : end - 1] + for token in [",", " ", "object"]: + inner_tokens = remove_all(inner_tokens, token) + print(inner_tokens) + if len(inner_tokens) == 0: + del tokens[bases_start + 1 :end - 1] + if len(inner_tokens) == 1: + del tokens[bases_start + 1 :end - 1] + tokens.insert(bases_start + 1, Token("NAME", inner_tokens[0])) + tokens.insert(bases_start + 2, Token("OP", ",")) @register(ast.Call) @@ -51,7 +59,9 @@ def visit_Call( ): for base in node.args[1].elts: if isinstance(base, ast.Name) and base.id == "object": + # TODO: send idx of the found object func = functools.partial( - remove_base_class_from_type_call, count=len(node.args[1].elts) + remove_base_class_from_type_call, + node_objs_count=len(node.args[1].elts), ) yield ast_to_offset(base), func From 51df918476494bf22320c532bb82b708c7722122 Mon Sep 17 00:00:00 2001 From: colin99d Date: Fri, 23 Dec 2022 08:27:18 -0500 Subject: [PATCH 03/15] Got it working when all on same line --- pyupgrade/_plugins/type_bases_object.py | 31 ++++++------ tests/features/type_bases_object_test.py | 62 ++++++++++++------------ 2 files changed, 47 insertions(+), 46 deletions(-) diff --git a/pyupgrade/_plugins/type_bases_object.py b/pyupgrade/_plugins/type_bases_object.py index f6440c83..13a0c6ae 100644 --- a/pyupgrade/_plugins/type_bases_object.py +++ b/pyupgrade/_plugins/type_bases_object.py @@ -22,23 +22,24 @@ def remove_all(the_list: list[str], item: str) -> list[str]: def remove_base_class_from_type_call( - i: int, tokens: list[Token], *, node_objs_count: int + _: int, tokens: list[Token], *, arguments: list[ast.Name] ) -> None: - token_list = [x.src for x in tokens] - print(tokens) + inner_tokens = [x.id for x in arguments] type_start = find_open_paren(tokens, 0) bases_start = find_open_paren(tokens, type_start + 1) - bases, end = parse_call_args(tokens, bases_start) - inner_tokens = token_list[bases_start + 1 : end - 1] - for token in [",", " ", "object"]: - inner_tokens = remove_all(inner_tokens, token) - print(inner_tokens) - if len(inner_tokens) == 0: - del tokens[bases_start + 1 :end - 1] - if len(inner_tokens) == 1: - del tokens[bases_start + 1 :end - 1] - tokens.insert(bases_start + 1, Token("NAME", inner_tokens[0])) - tokens.insert(bases_start + 2, Token("OP", ",")) + _, end = parse_call_args(tokens, bases_start) + inner_tokens = remove_all(inner_tokens, "object") + del tokens[bases_start + 1 :end - 1] + count = 1 + for i, token in enumerate(inner_tokens): + tokens.insert(bases_start + count, Token("NAME", token)) + count += 1 + if i != len(inner_tokens) - 1: + tokens.insert(bases_start + count, Token("UNIMPORTANT_WS", " ")) + tokens.insert(bases_start + count, Token("OP", ",")) + count += 2 + elif len(inner_tokens) == 1: + tokens.insert(bases_start + count, Token("OP", ",")) @register(ast.Call) @@ -62,6 +63,6 @@ def visit_Call( # TODO: send idx of the found object func = functools.partial( remove_base_class_from_type_call, - node_objs_count=len(node.args[1].elts), + arguments=node.args[1].elts, ) yield ast_to_offset(base), func diff --git a/tests/features/type_bases_object_test.py b/tests/features/type_bases_object_test.py index e1ff0fad..0c3d92bc 100644 --- a/tests/features/type_bases_object_test.py +++ b/tests/features/type_bases_object_test.py @@ -14,37 +14,6 @@ def test_fix_type_bases_object_noop(src): ret = _fix_plugins(src, settings=Settings()) assert ret == src """ -pytest.param( - 'C = type("C", (object, foo, bar), {})', - 'C = type("C", (foo, bar), {})', - id='three base classes, object first', -), -pytest.param( - 'D = type("D", (tuple, object), {})', - 'D = type("D", (tuple,), {})', - id='two base classes, object last', -), -pytest.param( - 'E = type("E", (foo, bar, object), {})', - 'E = type("E", (foo, bar), {})', - id='three base classes, object last', -), -pytest.param( - 'F = type(\n "F",\n (object, tuple),\n {}\n)', - 'F = type(\n "F",\n (tuple,),\n {}\n)', - id='newline and indent, two base classes', -), -pytest.param( - 'G = type(\n "G",\n (\n object,\n class1,\n' - ' class2,\n class3,\n class4,\n class5' - ',\n class6,\n class7,\n class8,\n ' - 'class9,\n classA,\n classB\n ),\n {}\n)', - 'G = type(\n "G",\n (\n class1,\n class2,\n' - ' class3,\n class4,\n class5,\n class6' - ',\n class7,\n class8,\n class9,\n ' - 'classA,\n classB\n ),\n {}\n)', - id='newline and also inside classes tuple', -), pytest.param( 'H = type(\n "H",\n (tuple, object),\n {}\n)', 'H = type(\n "H",\n (tuple,),\n {}\n)', @@ -137,6 +106,37 @@ def test_fix_type_bases_object_noop(src): 'B = type("B", (tuple,), {})', id='two base classes, object first', ), + pytest.param( + 'C = type("C", (object, foo, bar), {})', + 'C = type("C", (foo, bar), {})', + id='three base classes, object first', + ), + pytest.param( + 'D = type("D", (tuple, object), {})', + 'D = type("D", (tuple,), {})', + id='two base classes, object last', + ), + pytest.param( + 'E = type("E", (foo, bar, object), {})', + 'E = type("E", (foo, bar), {})', + id='three base classes, object last', + ), + pytest.param( + 'F = type(\n "F",\n (object, tuple),\n {}\n)', + 'F = type(\n "F",\n (tuple,),\n {}\n)', + id='newline and indent, two base classes', + ), + pytest.param( + 'G = type(\n "G",\n (\n object,\n class1,\n' + ' class2,\n class3,\n class4,\n class5' + ',\n class6,\n class7,\n class8,\n ' + 'class9,\n classA,\n classB\n ),\n {}\n)', + 'G = type(\n "G",\n (\n class1,\n class2,\n' + ' class3,\n class4,\n class5,\n class6' + ',\n class7,\n class8,\n class9,\n ' + 'classA,\n classB\n ),\n {}\n)', + id='newline and also inside classes tuple', + ), ), ) def test_fix_type_bases_object(s, expected): From 6b1451137daa85489897bd027f36fde6d358247c Mon Sep 17 00:00:00 2001 From: colin99d Date: Fri, 23 Dec 2022 10:08:23 -0500 Subject: [PATCH 04/15] Got further with mulitline --- pyupgrade/_plugins/type_bases_object.py | 25 +++++++++++++++--- tests/features/type_bases_object_test.py | 32 ++++++++++++------------ 2 files changed, 38 insertions(+), 19 deletions(-) diff --git a/pyupgrade/_plugins/type_bases_object.py b/pyupgrade/_plugins/type_bases_object.py index 13a0c6ae..8e909682 100644 --- a/pyupgrade/_plugins/type_bases_object.py +++ b/pyupgrade/_plugins/type_bases_object.py @@ -21,20 +21,39 @@ def remove_all(the_list: list[str], item: str) -> list[str]: return [x for x in the_list if x != item] +def remove_line(the_list: list[str], item: str) -> list[str]: + idx = [x.src for x in the_list].index(item) + del the_list[idx + 1] + del the_list[idx + 1] + del the_list[idx - 1] + del the_list[idx - 1] + + def remove_base_class_from_type_call( _: int, tokens: list[Token], *, arguments: list[ast.Name] ) -> None: - inner_tokens = [x.id for x in arguments] type_start = find_open_paren(tokens, 0) bases_start = find_open_paren(tokens, type_start + 1) _, end = parse_call_args(tokens, bases_start) + inner_tokens = tokens[bases_start + 1 : end - 1] + new_lines = [x.src for x in inner_tokens if x.name == "NL"] + names = [x.src for x in inner_tokens if x.name == "NAME"] + multi_line = len(new_lines) >= len(names) + targets = ["NAME", "NL"] + if multi_line: + targets.append("UNIMPORTANT_WS") + inner_tokens = [x.src for x in inner_tokens if x.name in targets] + if multi_line: + print("MULTI LINE") + remove_line(tokens, "object") + return inner_tokens = remove_all(inner_tokens, "object") - del tokens[bases_start + 1 :end - 1] + del tokens[bases_start + 1 : end - 1] count = 1 for i, token in enumerate(inner_tokens): tokens.insert(bases_start + count, Token("NAME", token)) count += 1 - if i != len(inner_tokens) - 1: + if i != len(inner_tokens) - 1 and token != "\n": tokens.insert(bases_start + count, Token("UNIMPORTANT_WS", " ")) tokens.insert(bases_start + count, Token("OP", ",")) count += 2 diff --git a/tests/features/type_bases_object_test.py b/tests/features/type_bases_object_test.py index 0c3d92bc..4d8266f2 100644 --- a/tests/features/type_bases_object_test.py +++ b/tests/features/type_bases_object_test.py @@ -14,22 +14,6 @@ def test_fix_type_bases_object_noop(src): ret = _fix_plugins(src, settings=Settings()) assert ret == src """ -pytest.param( - 'H = type(\n "H",\n (tuple, object),\n {}\n)', - 'H = type(\n "H",\n (tuple,),\n {}\n)', - id='newline and indent, two base classes, object last', -), -pytest.param( - 'I = type(\n "I",\n (\n class1,\n' - ' class2,\n class3,\n class4,\n class5' - ',\n class6,\n class7,\n class8,\n ' - 'class9,\n classA,\n object\n ),\n {}\n)', - 'I = type(\n "I",\n (\n class1,\n class2,\n' - ' class3,\n class4,\n class5,\n class6' - ',\n class7,\n class8,\n class9,\n ' - 'classA\n ),\n {}\n)', - id='newline and also inside classes tuple, object last', -), pytest.param( 'J = type("J", (object, foo, bar,), {})', 'J = type("J", (foo, bar,), {})', @@ -137,6 +121,22 @@ def test_fix_type_bases_object_noop(src): 'classA,\n classB\n ),\n {}\n)', id='newline and also inside classes tuple', ), + pytest.param( + 'H = type(\n "H",\n (tuple, object),\n {}\n)', + 'H = type(\n "H",\n (tuple,),\n {}\n)', + id='newline and indent, two base classes, object last', + ), + pytest.param( + 'I = type(\n "I",\n (\n class1,\n' + ' class2,\n class3,\n class4,\n class5' + ',\n class6,\n class7,\n class8,\n ' + 'class9,\n classA,\n object\n ),\n {}\n)', + 'I = type(\n "I",\n (\n class1,\n class2,\n' + ' class3,\n class4,\n class5,\n class6' + ',\n class7,\n class8,\n class9,\n ' + 'classA\n ),\n {}\n)', + id='newline and also inside classes tuple, object last', + ), ), ) def test_fix_type_bases_object(s, expected): From 9175e20603abfbb65f192b8f03e327c8a2e4ad12 Mon Sep 17 00:00:00 2001 From: colin99d Date: Fri, 23 Dec 2022 11:51:11 -0500 Subject: [PATCH 05/15] Handled more edge cases --- pyupgrade/_plugins/type_bases_object.py | 27 ++++++++++++++------- tests/features/type_bases_object_test.py | 30 ++++++++++++------------ 2 files changed, 33 insertions(+), 24 deletions(-) diff --git a/pyupgrade/_plugins/type_bases_object.py b/pyupgrade/_plugins/type_bases_object.py index 8e909682..cb689878 100644 --- a/pyupgrade/_plugins/type_bases_object.py +++ b/pyupgrade/_plugins/type_bases_object.py @@ -21,44 +21,53 @@ def remove_all(the_list: list[str], item: str) -> list[str]: return [x for x in the_list if x != item] -def remove_line(the_list: list[str], item: str) -> list[str]: +def remove_line(the_list: list[str], sub_list: list[str], item: str) -> list[str]: + is_last = sub_list[-1] == item idx = [x.src for x in the_list].index(item) - del the_list[idx + 1] - del the_list[idx + 1] - del the_list[idx - 1] - del the_list[idx - 1] + line = the_list[idx].line + idxs = ([i for i, x in enumerate(the_list) if x.line == line]) + del the_list[min(idxs):max(idxs)+1] + if is_last: + del the_list[min(idxs)-2] def remove_base_class_from_type_call( _: int, tokens: list[Token], *, arguments: list[ast.Name] ) -> None: + print([x.src for x in tokens]) type_start = find_open_paren(tokens, 0) bases_start = find_open_paren(tokens, type_start + 1) _, end = parse_call_args(tokens, bases_start) inner_tokens = tokens[bases_start + 1 : end - 1] + last_is_comma = inner_tokens[-1].src == ',' new_lines = [x.src for x in inner_tokens if x.name == "NL"] names = [x.src for x in inner_tokens if x.name == "NAME"] multi_line = len(new_lines) >= len(names) targets = ["NAME", "NL"] if multi_line: - targets.append("UNIMPORTANT_WS") + targets.remove("NL") inner_tokens = [x.src for x in inner_tokens if x.name in targets] + # This gets run if the function arguments are spread out over multiple lines if multi_line: - print("MULTI LINE") - remove_line(tokens, "object") + remove_line(tokens, inner_tokens, "object") return inner_tokens = remove_all(inner_tokens, "object") del tokens[bases_start + 1 : end - 1] count = 1 + object_is_last = names[-1] == "object" for i, token in enumerate(inner_tokens): + # Boolean value to see if the current item is the last + last = i == len(inner_tokens) - 1 tokens.insert(bases_start + count, Token("NAME", token)) count += 1 - if i != len(inner_tokens) - 1 and token != "\n": + if not last and token != "\n": tokens.insert(bases_start + count, Token("UNIMPORTANT_WS", " ")) tokens.insert(bases_start + count, Token("OP", ",")) count += 2 elif len(inner_tokens) == 1: tokens.insert(bases_start + count, Token("OP", ",")) + elif last_is_comma: + tokens.insert(bases_start + count, Token("OP", ",")) @register(ast.Call) diff --git a/tests/features/type_bases_object_test.py b/tests/features/type_bases_object_test.py index 4d8266f2..d9edd4d2 100644 --- a/tests/features/type_bases_object_test.py +++ b/tests/features/type_bases_object_test.py @@ -14,21 +14,6 @@ def test_fix_type_bases_object_noop(src): ret = _fix_plugins(src, settings=Settings()) assert ret == src """ -pytest.param( - 'J = type("J", (object, foo, bar,), {})', - 'J = type("J", (foo, bar,), {})', - id='trailing comma, object first', -), -pytest.param( - 'K = type("K", (foo, bar, object,), {})', - 'K = type("K", (foo, bar,), {})', - id='trailing comma, object last', -), -pytest.param( - 'L = type(\n "L",\n (foo, bar, object,),\n {}\n)', - 'L = type(\n "L",\n (foo, bar,),\n {}\n)', - id='trailing comma, newline and indent, object last', -), pytest.param( 'M = type(\n "M",\n (\n class1,\n' ' class2,\n class3,\n class4,\n class5' @@ -137,6 +122,21 @@ def test_fix_type_bases_object_noop(src): 'classA\n ),\n {}\n)', id='newline and also inside classes tuple, object last', ), + pytest.param( + 'J = type("J", (object, foo, bar,), {})', + 'J = type("J", (foo, bar,), {})', + id='trailing comma, object first', + ), + pytest.param( + 'K = type("K", (foo, bar, object,), {})', + 'K = type("K", (foo, bar,), {})', + id='trailing comma, object last', + ), + pytest.param( + 'L = type(\n "L",\n (foo, bar, object,),\n {}\n)', + 'L = type(\n "L",\n (foo, bar,),\n {}\n)', + id='trailing comma, newline and indent, object last', + ), ), ) def test_fix_type_bases_object(s, expected): From 147de58421adf256987f7fd58babe5b5421b826d Mon Sep 17 00:00:00 2001 From: colin99d Date: Fri, 23 Dec 2022 12:02:39 -0500 Subject: [PATCH 06/15] Handled a bunch more edge cases --- pyupgrade/_plugins/type_bases_object.py | 15 ++++-- tests/features/type_bases_object_test.py | 66 ++++++++++++------------ 2 files changed, 43 insertions(+), 38 deletions(-) diff --git a/pyupgrade/_plugins/type_bases_object.py b/pyupgrade/_plugins/type_bases_object.py index cb689878..7c4f1baf 100644 --- a/pyupgrade/_plugins/type_bases_object.py +++ b/pyupgrade/_plugins/type_bases_object.py @@ -17,31 +17,36 @@ from pyupgrade._token_helpers import delete_argument +def is_last_comma(tokens: list[Token], names: list[str]) -> bool: + last_arg = names[-1] + idx = [x.src for x in tokens].index(last_arg) + return tokens[idx+1].src == "," + + def remove_all(the_list: list[str], item: str) -> list[str]: return [x for x in the_list if x != item] -def remove_line(the_list: list[str], sub_list: list[str], item: str) -> list[str]: +def remove_line(the_list: list[str], sub_list: list[str], item: str, last_is_comma: bool) -> list[str]: is_last = sub_list[-1] == item idx = [x.src for x in the_list].index(item) line = the_list[idx].line idxs = ([i for i, x in enumerate(the_list) if x.line == line]) del the_list[min(idxs):max(idxs)+1] - if is_last: + if is_last and not last_is_comma: del the_list[min(idxs)-2] def remove_base_class_from_type_call( _: int, tokens: list[Token], *, arguments: list[ast.Name] ) -> None: - print([x.src for x in tokens]) type_start = find_open_paren(tokens, 0) bases_start = find_open_paren(tokens, type_start + 1) _, end = parse_call_args(tokens, bases_start) inner_tokens = tokens[bases_start + 1 : end - 1] - last_is_comma = inner_tokens[-1].src == ',' new_lines = [x.src for x in inner_tokens if x.name == "NL"] names = [x.src for x in inner_tokens if x.name == "NAME"] + last_is_comma = is_last_comma(tokens, names) multi_line = len(new_lines) >= len(names) targets = ["NAME", "NL"] if multi_line: @@ -49,7 +54,7 @@ def remove_base_class_from_type_call( inner_tokens = [x.src for x in inner_tokens if x.name in targets] # This gets run if the function arguments are spread out over multiple lines if multi_line: - remove_line(tokens, inner_tokens, "object") + remove_line(tokens, inner_tokens, "object", last_is_comma) return inner_tokens = remove_all(inner_tokens, "object") del tokens[bases_start + 1 : end - 1] diff --git a/tests/features/type_bases_object_test.py b/tests/features/type_bases_object_test.py index d9edd4d2..0829a7ad 100644 --- a/tests/features/type_bases_object_test.py +++ b/tests/features/type_bases_object_test.py @@ -14,24 +14,6 @@ def test_fix_type_bases_object_noop(src): ret = _fix_plugins(src, settings=Settings()) assert ret == src """ -pytest.param( - 'M = type(\n "M",\n (\n class1,\n' - ' class2,\n class3,\n class4,\n class5' - ',\n class6,\n class7,\n class8,\n ' - 'class9,\n classA,\n object,\n ),\n {}\n)', - 'M = type(\n "M",\n (\n class1,\n class2,\n' - ' class3,\n class4,\n class5,\n class6' - ',\n class7,\n class8,\n class9,\n ' - 'classA,\n ),\n {}\n)', - id='trailing comma, ' - 'newline and also inside classes tuple, ' - 'object last', -), -pytest.param( - 'O = type("O", (foo, object, bar), {})', - 'O = type("O", (foo, bar), {})', - id='object in the middle', -), pytest.param( 'P = type( \n"P",\n (\n foo,\n object,' '\n bar\n ),\n {}\n)', @@ -44,21 +26,6 @@ def test_fix_type_bases_object_noop(src): 'Q = type(\n "Q",\n (foo, bar),\n {}\n)', id='newline and indent, object in the middle', ), -pytest.param( - 'R = type("R", (object,tuple), {})', - 'R = type("R", (tuple,), {})', - id='no spaces, object first', -), -pytest.param( - 'S = type("S", (tuple,object), {})', - 'S = type("S", (tuple,), {})', - id='no spaces, object last', -), -pytest.param( - 'U = type("U", (tuple, object,), {})', - 'U = type("U", (tuple,), {})', - id='trailing comma, object last, two classes', -), """ @@ -137,6 +104,39 @@ def test_fix_type_bases_object_noop(src): 'L = type(\n "L",\n (foo, bar,),\n {}\n)', id='trailing comma, newline and indent, object last', ), + pytest.param( + 'M = type(\n "M",\n (\n class1,\n' + ' class2,\n class3,\n class4,\n class5' + ',\n class6,\n class7,\n class8,\n ' + 'class9,\n classA,\n object,\n ),\n {}\n)', + 'M = type(\n "M",\n (\n class1,\n class2,\n' + ' class3,\n class4,\n class5,\n class6' + ',\n class7,\n class8,\n class9,\n ' + 'classA,\n ),\n {}\n)', + id='trailing comma, ' + 'newline and also inside classes tuple, ' + 'object last', + ), + pytest.param( + 'O = type("O", (foo, object, bar), {})', + 'O = type("O", (foo, bar), {})', + id='object in the middle', + ), + pytest.param( + 'R = type("R", (object,tuple), {})', + 'R = type("R", (tuple,), {})', + id='no spaces, object first', + ), + pytest.param( + 'S = type("S", (tuple,object), {})', + 'S = type("S", (tuple,), {})', + id='no spaces, object last', + ), + pytest.param( + 'U = type("U", (tuple, object,), {})', + 'U = type("U", (tuple,), {})', + id='trailing comma, object last, two classes', + ), ), ) def test_fix_type_bases_object(s, expected): From 5d5a5400e3f5d2522e8bd3e2734d2739089171cf Mon Sep 17 00:00:00 2001 From: colin99d Date: Fri, 23 Dec 2022 12:03:42 -0500 Subject: [PATCH 07/15] Finished getting all tests to pass --- tests/features/type_bases_object_test.py | 26 +++++++++++------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/tests/features/type_bases_object_test.py b/tests/features/type_bases_object_test.py index 0829a7ad..c37eaa02 100644 --- a/tests/features/type_bases_object_test.py +++ b/tests/features/type_bases_object_test.py @@ -13,20 +13,6 @@ def test_fix_type_bases_object_noop(src): ret = _fix_plugins(src, settings=Settings()) assert ret == src -""" -pytest.param( - 'P = type( \n"P",\n (\n foo,\n object,' - '\n bar\n ),\n {}\n)', - 'P = type( \n"P",\n (\n foo,\n bar\n ' - '),\n {}\n)', - id='newline and also inside classes tuple, object in the middle', -), -pytest.param( - 'Q = type(\n "Q",\n (foo, object, bar),\n {}\n)', - 'Q = type(\n "Q",\n (foo, bar),\n {}\n)', - id='newline and indent, object in the middle', -), -""" @pytest.mark.parametrize( @@ -137,6 +123,18 @@ def test_fix_type_bases_object_noop(src): 'U = type("U", (tuple,), {})', id='trailing comma, object last, two classes', ), + pytest.param( + 'P = type( \n"P",\n (\n foo,\n object,' + '\n bar\n ),\n {}\n)', + 'P = type( \n"P",\n (\n foo,\n bar\n ' + '),\n {}\n)', + id='newline and also inside classes tuple, object in the middle', + ), + pytest.param( + 'Q = type(\n "Q",\n (foo, object, bar),\n {}\n)', + 'Q = type(\n "Q",\n (foo, bar),\n {}\n)', + id='newline and indent, object in the middle', + ), ), ) def test_fix_type_bases_object(s, expected): From c54e7315c2d1376cf34a9bc809e0a56048c01aed Mon Sep 17 00:00:00 2001 From: colin99d Date: Fri, 23 Dec 2022 12:08:12 -0500 Subject: [PATCH 08/15] Cleaned up code --- pyupgrade/_plugins/type_bases_object.py | 33 ++++++++++--------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/pyupgrade/_plugins/type_bases_object.py b/pyupgrade/_plugins/type_bases_object.py index 7c4f1baf..7b50f0f1 100644 --- a/pyupgrade/_plugins/type_bases_object.py +++ b/pyupgrade/_plugins/type_bases_object.py @@ -1,7 +1,6 @@ from __future__ import annotations import ast -import functools from typing import Iterable from tokenize_rt import Offset @@ -14,32 +13,31 @@ from pyupgrade._data import TokenFunc from pyupgrade._token_helpers import find_open_paren from pyupgrade._token_helpers import parse_call_args -from pyupgrade._token_helpers import delete_argument def is_last_comma(tokens: list[Token], names: list[str]) -> bool: last_arg = names[-1] idx = [x.src for x in tokens].index(last_arg) - return tokens[idx+1].src == "," + return tokens[idx + 1].src == "," def remove_all(the_list: list[str], item: str) -> list[str]: return [x for x in the_list if x != item] -def remove_line(the_list: list[str], sub_list: list[str], item: str, last_is_comma: bool) -> list[str]: +def remove_line( + the_list: list[str], sub_list: list[str], item: str, last_is_comma: bool +) -> list[str]: is_last = sub_list[-1] == item idx = [x.src for x in the_list].index(item) line = the_list[idx].line - idxs = ([i for i, x in enumerate(the_list) if x.line == line]) - del the_list[min(idxs):max(idxs)+1] + idxs = [i for i, x in enumerate(the_list) if x.line == line] + del the_list[min(idxs) : max(idxs) + 1] if is_last and not last_is_comma: - del the_list[min(idxs)-2] + del the_list[min(idxs) - 2] -def remove_base_class_from_type_call( - _: int, tokens: list[Token], *, arguments: list[ast.Name] -) -> None: +def remove_base_class_from_type_call(_: int, tokens: list[Token]) -> None: type_start = find_open_paren(tokens, 0) bases_start = find_open_paren(tokens, type_start + 1) _, end = parse_call_args(tokens, bases_start) @@ -57,21 +55,21 @@ def remove_base_class_from_type_call( remove_line(tokens, inner_tokens, "object", last_is_comma) return inner_tokens = remove_all(inner_tokens, "object") + # start by deleting all tokens, we will selectively add back del tokens[bases_start + 1 : end - 1] count = 1 - object_is_last = names[-1] == "object" for i, token in enumerate(inner_tokens): # Boolean value to see if the current item is the last last = i == len(inner_tokens) - 1 tokens.insert(bases_start + count, Token("NAME", token)) count += 1 + # adds a comma and a space if the current item is not the last if not last and token != "\n": tokens.insert(bases_start + count, Token("UNIMPORTANT_WS", " ")) tokens.insert(bases_start + count, Token("OP", ",")) count += 2 - elif len(inner_tokens) == 1: - tokens.insert(bases_start + count, Token("OP", ",")) - elif last_is_comma: + # If the lenght is only one, or the last one had a comma, add a comma + elif (last and last_is_comma) or len(inner_tokens) == 1: tokens.insert(bases_start + count, Token("OP", ",")) @@ -93,9 +91,4 @@ def visit_Call( ): for base in node.args[1].elts: if isinstance(base, ast.Name) and base.id == "object": - # TODO: send idx of the found object - func = functools.partial( - remove_base_class_from_type_call, - arguments=node.args[1].elts, - ) - yield ast_to_offset(base), func + yield ast_to_offset(base), remove_base_class_from_type_call From 1cdc0a5961543d044833a8e5e8fba2d094929a55 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 23 Dec 2022 17:11:08 +0000 Subject: [PATCH 09/15] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- pyupgrade/_plugins/type_bases_object.py | 46 ++++++++++++------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/pyupgrade/_plugins/type_bases_object.py b/pyupgrade/_plugins/type_bases_object.py index 7b50f0f1..ba1759ad 100644 --- a/pyupgrade/_plugins/type_bases_object.py +++ b/pyupgrade/_plugins/type_bases_object.py @@ -18,7 +18,7 @@ def is_last_comma(tokens: list[Token], names: list[str]) -> bool: last_arg = names[-1] idx = [x.src for x in tokens].index(last_arg) - return tokens[idx + 1].src == "," + return tokens[idx + 1].src == ',' def remove_all(the_list: list[str], item: str) -> list[str]: @@ -26,13 +26,13 @@ def remove_all(the_list: list[str], item: str) -> list[str]: def remove_line( - the_list: list[str], sub_list: list[str], item: str, last_is_comma: bool + the_list: list[str], sub_list: list[str], item: str, last_is_comma: bool, ) -> list[str]: is_last = sub_list[-1] == item idx = [x.src for x in the_list].index(item) line = the_list[idx].line idxs = [i for i, x in enumerate(the_list) if x.line == line] - del the_list[min(idxs) : max(idxs) + 1] + del the_list[min(idxs): max(idxs) + 1] if is_last and not last_is_comma: del the_list[min(idxs) - 2] @@ -41,36 +41,36 @@ def remove_base_class_from_type_call(_: int, tokens: list[Token]) -> None: type_start = find_open_paren(tokens, 0) bases_start = find_open_paren(tokens, type_start + 1) _, end = parse_call_args(tokens, bases_start) - inner_tokens = tokens[bases_start + 1 : end - 1] - new_lines = [x.src for x in inner_tokens if x.name == "NL"] - names = [x.src for x in inner_tokens if x.name == "NAME"] + inner_tokens = tokens[bases_start + 1: end - 1] + new_lines = [x.src for x in inner_tokens if x.name == 'NL'] + names = [x.src for x in inner_tokens if x.name == 'NAME'] last_is_comma = is_last_comma(tokens, names) multi_line = len(new_lines) >= len(names) - targets = ["NAME", "NL"] + targets = ['NAME', 'NL'] if multi_line: - targets.remove("NL") + targets.remove('NL') inner_tokens = [x.src for x in inner_tokens if x.name in targets] # This gets run if the function arguments are spread out over multiple lines if multi_line: - remove_line(tokens, inner_tokens, "object", last_is_comma) + remove_line(tokens, inner_tokens, 'object', last_is_comma) return - inner_tokens = remove_all(inner_tokens, "object") + inner_tokens = remove_all(inner_tokens, 'object') # start by deleting all tokens, we will selectively add back - del tokens[bases_start + 1 : end - 1] + del tokens[bases_start + 1: end - 1] count = 1 for i, token in enumerate(inner_tokens): # Boolean value to see if the current item is the last last = i == len(inner_tokens) - 1 - tokens.insert(bases_start + count, Token("NAME", token)) + tokens.insert(bases_start + count, Token('NAME', token)) count += 1 # adds a comma and a space if the current item is not the last - if not last and token != "\n": - tokens.insert(bases_start + count, Token("UNIMPORTANT_WS", " ")) - tokens.insert(bases_start + count, Token("OP", ",")) + if not last and token != '\n': + tokens.insert(bases_start + count, Token('UNIMPORTANT_WS', ' ')) + tokens.insert(bases_start + count, Token('OP', ',')) count += 2 # If the lenght is only one, or the last one had a comma, add a comma elif (last and last_is_comma) or len(inner_tokens) == 1: - tokens.insert(bases_start + count, Token("OP", ",")) + tokens.insert(bases_start + count, Token('OP', ',')) @register(ast.Call) @@ -80,15 +80,15 @@ def visit_Call( parent: ast.AST, ) -> Iterable[tuple[Offset, TokenFunc]]: if ( - isinstance(node.func, ast.Name) - and node.func.id == "type" - and len(node.args) > 1 - and isinstance(node.args[1], ast.Tuple) - and any( - isinstance(elt, ast.Name) and elt.id == "object" + isinstance(node.func, ast.Name) and + node.func.id == 'type' and + len(node.args) > 1 and + isinstance(node.args[1], ast.Tuple) and + any( + isinstance(elt, ast.Name) and elt.id == 'object' for elt in node.args[1].elts ) ): for base in node.args[1].elts: - if isinstance(base, ast.Name) and base.id == "object": + if isinstance(base, ast.Name) and base.id == 'object': yield ast_to_offset(base), remove_base_class_from_type_call From 45a7483eefb337a83c51a53139e8d11f7e76957c Mon Sep 17 00:00:00 2001 From: colin99d Date: Fri, 23 Dec 2022 12:14:19 -0500 Subject: [PATCH 10/15] CI Fixes --- pyupgrade/_plugins/type_bases_object.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pyupgrade/_plugins/type_bases_object.py b/pyupgrade/_plugins/type_bases_object.py index 7b50f0f1..9ca52da7 100644 --- a/pyupgrade/_plugins/type_bases_object.py +++ b/pyupgrade/_plugins/type_bases_object.py @@ -5,7 +5,6 @@ from tokenize_rt import Offset from tokenize_rt import Token -from tokenize_rt import UNIMPORTANT_WS from pyupgrade._ast_helpers import ast_to_offset from pyupgrade._data import register @@ -26,7 +25,7 @@ def remove_all(the_list: list[str], item: str) -> list[str]: def remove_line( - the_list: list[str], sub_list: list[str], item: str, last_is_comma: bool + the_list: list[Token], sub_list: list[str], item: str, last_is_comma: bool ) -> list[str]: is_last = sub_list[-1] == item idx = [x.src for x in the_list].index(item) @@ -50,7 +49,7 @@ def remove_base_class_from_type_call(_: int, tokens: list[Token]) -> None: if multi_line: targets.remove("NL") inner_tokens = [x.src for x in inner_tokens if x.name in targets] - # This gets run if the function arguments are spread out over multiple lines + # This gets run if the function arguments are on over multiple lines if multi_line: remove_line(tokens, inner_tokens, "object", last_is_comma) return From d3db5f0cd6a7d95dd599e3e14410ddf2c5adf297 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 23 Dec 2022 17:15:11 +0000 Subject: [PATCH 11/15] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- pyupgrade/_plugins/type_bases_object.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyupgrade/_plugins/type_bases_object.py b/pyupgrade/_plugins/type_bases_object.py index f8e22378..862f142b 100644 --- a/pyupgrade/_plugins/type_bases_object.py +++ b/pyupgrade/_plugins/type_bases_object.py @@ -25,11 +25,11 @@ def remove_all(the_list: list[str], item: str) -> list[str]: def remove_line( -<<<<<<< HEAD - the_list: list[Token], sub_list: list[str], item: str, last_is_comma: bool -======= + << << << < HEAD + the_list: list[Token], sub_list: list[str], item: str, last_is_comma: bool == + == == = the_list: list[str], sub_list: list[str], item: str, last_is_comma: bool, ->>>>>>> 1cdc0a5961543d044833a8e5e8fba2d094929a55 + >>>>>> > 1cdc0a5961543d044833a8e5e8fba2d094929a55 ) -> list[str]: is_last = sub_list[-1] == item idx = [x.src for x in the_list].index(item) From 771b327c8fdc9afb18eae535bcc63fa80ede5300 Mon Sep 17 00:00:00 2001 From: colin99d Date: Fri, 23 Dec 2022 12:15:11 -0500 Subject: [PATCH 12/15] CI Fixes --- pyupgrade/_plugins/type_bases_object.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pyupgrade/_plugins/type_bases_object.py b/pyupgrade/_plugins/type_bases_object.py index f8e22378..a21fe0d2 100644 --- a/pyupgrade/_plugins/type_bases_object.py +++ b/pyupgrade/_plugins/type_bases_object.py @@ -25,11 +25,7 @@ def remove_all(the_list: list[str], item: str) -> list[str]: def remove_line( -<<<<<<< HEAD the_list: list[Token], sub_list: list[str], item: str, last_is_comma: bool -======= - the_list: list[str], sub_list: list[str], item: str, last_is_comma: bool, ->>>>>>> 1cdc0a5961543d044833a8e5e8fba2d094929a55 ) -> list[str]: is_last = sub_list[-1] == item idx = [x.src for x in the_list].index(item) From 279542b28f32babc82e98a0ecd1748e8fec68fc1 Mon Sep 17 00:00:00 2001 From: colin99d Date: Fri, 23 Dec 2022 12:20:17 -0500 Subject: [PATCH 13/15] CI Fixes --- pyupgrade/_plugins/type_bases_object.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyupgrade/_plugins/type_bases_object.py b/pyupgrade/_plugins/type_bases_object.py index a21fe0d2..86a8e8cf 100644 --- a/pyupgrade/_plugins/type_bases_object.py +++ b/pyupgrade/_plugins/type_bases_object.py @@ -26,7 +26,7 @@ def remove_all(the_list: list[str], item: str) -> list[str]: def remove_line( the_list: list[Token], sub_list: list[str], item: str, last_is_comma: bool -) -> list[str]: +) -> None: is_last = sub_list[-1] == item idx = [x.src for x in the_list].index(item) line = the_list[idx].line From b973b30b71a79987e3ac281d8eace84d681f3268 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 23 Dec 2022 17:20:35 +0000 Subject: [PATCH 14/15] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- pyupgrade/_plugins/type_bases_object.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyupgrade/_plugins/type_bases_object.py b/pyupgrade/_plugins/type_bases_object.py index 86a8e8cf..92be0471 100644 --- a/pyupgrade/_plugins/type_bases_object.py +++ b/pyupgrade/_plugins/type_bases_object.py @@ -25,7 +25,7 @@ def remove_all(the_list: list[str], item: str) -> list[str]: def remove_line( - the_list: list[Token], sub_list: list[str], item: str, last_is_comma: bool + the_list: list[Token], sub_list: list[str], item: str, last_is_comma: bool, ) -> None: is_last = sub_list[-1] == item idx = [x.src for x in the_list].index(item) From cd79bb35a330ea6c78acf1973cdb9e4d8a2ccc97 Mon Sep 17 00:00:00 2001 From: colin99d Date: Fri, 23 Dec 2022 12:24:15 -0500 Subject: [PATCH 15/15] CI Fixes --- pyupgrade/_plugins/type_bases_object.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyupgrade/_plugins/type_bases_object.py b/pyupgrade/_plugins/type_bases_object.py index 86a8e8cf..c25543a1 100644 --- a/pyupgrade/_plugins/type_bases_object.py +++ b/pyupgrade/_plugins/type_bases_object.py @@ -39,7 +39,7 @@ def remove_line( def remove_base_class_from_type_call(_: int, tokens: list[Token]) -> None: type_start = find_open_paren(tokens, 0) bases_start = find_open_paren(tokens, type_start + 1) - _, end = parse_call_args(tokens, bases_start) + bases, end = parse_call_args(tokens, bases_start) inner_tokens = tokens[bases_start + 1: end - 1] new_lines = [x.src for x in inner_tokens if x.name == 'NL'] names = [x.src for x in inner_tokens if x.name == 'NAME']