Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

remove object class from base classes in type calls #731

Closed
wants to merge 24 commits into from
Closed
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
d60e1fd
remove object class from base classes in ype calls
lev-blit Oct 8, 2022
e40695d
add the feature to the readme
lev-blit Oct 8, 2022
07ec3e9
rename class names to be different in each test case
lev-blit Oct 8, 2022
18f40d0
fix flake8 issues
lev-blit Oct 8, 2022
4a7ac70
Merge branch 'asottile:main' into main
lev-blit Oct 11, 2022
d23b8ba
join conditions into one
lev-blit Oct 11, 2022
c133114
remove unnecessary argument 'expected'
lev-blit Oct 11, 2022
fa1f82a
rewrite parameters with pytest.param
lev-blit Oct 11, 2022
8eb6d51
move documentation next to other relavant object documentation
lev-blit Oct 11, 2022
86a7fba
add two test cases with indents
lev-blit Oct 11, 2022
3a2c3d6
support newline and indents with object last
lev-blit Oct 11, 2022
9574679
add tests for trailing commas
lev-blit Oct 11, 2022
2abdb10
move function from shared pyupgrade/_token_helpers to pyupgrade/_plug…
lev-blit Oct 11, 2022
f1afe8e
use `remove_base_class` function for both class declarations and type…
lev-blit Oct 11, 2022
a43a666
use a similar message in readme as class declaration
lev-blit Oct 11, 2022
12eb5b9
use a similar message in readme as class declaration
lev-blit Oct 11, 2022
17c16aa
rewrite replacemnt function to use parse_call_args instead of going t…
lev-blit Oct 11, 2022
c2f16d4
add support for object in the middle of bases
lev-blit Oct 11, 2022
d819b55
fix handling of no spaces between bases
lev-blit Oct 11, 2022
bbba5fe
fix two trailing commas - (tuple, object,) was converting into (tuple,,)
lev-blit Oct 11, 2022
1413afc
add pragma: no cover to ignore empty bases(the function won't be call…
lev-blit Oct 11, 2022
1d51386
Merge remote-tracking branch 'origin/main'
lev-blit Oct 11, 2022
48f0681
Revert "use `remove_base_class` function for both class declarations …
lev-blit Oct 11, 2022
0d006e9
Merge branch 'asottile:main' into main
lev-blit Oct 22, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,12 @@ Rewrites [deprecated unittest method aliases](https://docs.python.org/3/library/
+class C(B): pass
```

#### rewrites creating classes with `type()`
```diff
-A = type("A", (object,), {})
+A = type("A", (), {})
```

#### removes `__metaclass__ = type` declaration

```diff
Expand Down
106 changes: 106 additions & 0 deletions pyupgrade/_plugins/type_bases_object.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
from __future__ import annotations

import ast
from typing import Iterable

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
from pyupgrade._token_helpers import remove_base_class


def _should_move_right_edge(src: str) -> bool:
return src != ')'


def _not_right_edge(token: Token) -> bool:
return token.src != ')' and token.name != 'NAME'


def _last_part_function(i: int, src: str) -> int:
return i if src == ',' else i + 1


def _remove_bases(tokens: list[Token], left: int, right: int) -> None:
del tokens[left + 1:right + 1]


def _multiple_first(tokens: list[Token], left: int, right: int) -> None:
# only one base will be left, (tuple) -> (tuple,)
if sum(
1 if token.name == 'NAME' else 0
for token in tokens[left:find_closing_bracket(tokens, left)]
) == 2:
tokens.insert(right + 1, Token('OP', ','))
# we should preserve the newline
if tokens[left + 1].name == 'NL':
# we should also preserve indents
if tokens[left + 2].name == 'UNIMPORTANT_WS':
del tokens[left + 3:right]
else:
del tokens[left + 2:right]
else:
del tokens[left + 1:right]


def _multiple_last(tokens: list[Token], left: int, last_part: int) -> None:
type_call_open = find_open_paren(tokens, 0)
bases_open = find_open_paren(tokens, type_call_open + 1)
# only one base will be left, (tuple) -> (tuple,)
if sum(
1 if token.name == 'NAME' else 0
for token in tokens[bases_open:last_part + 1]
) == 2:
del tokens[left + 1:last_part]
# we should preserve indents
elif tokens[last_part - 1].name == 'UNIMPORTANT_WS':
# we should also preserve the newline
if tokens[last_part - 2].name == 'NL':
del tokens[left:last_part - 2]
else:
del tokens[left:last_part - 1]
else:
del tokens[left:last_part]


def remove_base_class_from_type_call(i: int, tokens: list[Token]) -> None:
remove_base_class(
i,
tokens,
should_move_right_edge=_should_move_right_edge,
not_right_edge=_not_right_edge,
last_part_function=_last_part_function,
do_brace_stack_pop_loop=False,
remove_bases=_remove_bases,
single_base_char=',',
multiple_first_char=')',
multiple_first=_multiple_first,
multiple_last=_multiple_last,
)
lev-blit marked this conversation as resolved.
Show resolved Hide resolved


@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':
yield ast_to_offset(base), remove_base_class_from_type_call
56 changes: 40 additions & 16 deletions pyupgrade/_token_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import ast
import keyword
import sys
from typing import Callable
from typing import NamedTuple
from typing import Sequence

Expand Down Expand Up @@ -301,7 +302,20 @@ def remove_brace(tokens: list[Token], i: int) -> None:
del tokens[i]


def remove_base_class(i: int, tokens: list[Token]) -> None:
def remove_base_class(
i: int,
tokens: list[Token],
*,
should_move_right_edge: Callable[[str], bool] = lambda src: src == ')',
not_right_edge: Callable[[Token], bool] = lambda token: True,
last_part_function: Callable[[int, str], int] = lambda i, s: i,
do_brace_stack_pop_loop: bool = True,
remove_bases: Callable[[list[Token], int, int], None] | None = None,
single_base_char: str = ':',
multiple_first_char: str = ':',
multiple_first: Callable[[list[Token], int, int], None] | None = None,
multiple_last: Callable[[list[Token], int, int], None] | None = None,
) -> None:
# look forward and backward to find commas / parens
brace_stack = []
j = i
Expand All @@ -318,38 +332,48 @@ def remove_base_class(i: int, tokens: list[Token]) -> None:
j = right + 1
while tokens[j].name in NON_CODING_TOKENS:
j += 1
if tokens[j].src == ')':
while tokens[j].src != ':':
if should_move_right_edge(tokens[j].src):
while tokens[j].src != ':' and not_right_edge(tokens[j]):
j += 1
right = j

if brace_stack:
last_part = brace_stack[-1]
else:
last_part = i
last_part = last_part_function(i, tokens[i].src)

j = i
while brace_stack:
if tokens[j].src == '(':
brace_stack.pop()
j -= 1
if do_brace_stack_pop_loop:
while brace_stack:
if tokens[j].src == '(':
brace_stack.pop()
j -= 1

while tokens[j].src not in {',', '('}:
j -= 1
left = j

# single base, remove the entire bases
if tokens[left].src == '(' and tokens[right].src == ':':
del tokens[left:right]
if tokens[left].src == '(' and tokens[right].src == single_base_char:
if remove_bases is not None:
remove_bases(tokens, left, right)
else:
del tokens[left:right]
# multiple bases, base is first
elif tokens[left].src == '(' and tokens[right].src != ':':
# if there's space / comment afterwards remove that too
while tokens[right + 1].name in {UNIMPORTANT_WS, 'COMMENT'}:
right += 1
del tokens[left + 1:right + 1]
elif tokens[left].src == '(' and tokens[right].src != multiple_first_char:
if multiple_first is not None:
multiple_first(tokens, left, right)
else:
# if there's space / comment afterwards remove that too
while tokens[right + 1].name in {UNIMPORTANT_WS, 'COMMENT'}:
right += 1
del tokens[left + 1:right + 1]
# multiple bases, base is not first
else:
del tokens[left:last_part + 1]
if multiple_last is not None:
multiple_last(tokens, left, last_part)
else:
del tokens[left:last_part + 1]


def remove_decorator(i: int, tokens: list[Token]) -> None:
Expand Down
110 changes: 110 additions & 0 deletions tests/features/type_bases_object_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
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.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',
),
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), {})',
lev-blit marked this conversation as resolved.
Show resolved Hide resolved
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',
),
),
)
def test_fix_type_bases_object(s, expected):
ret = _fix_plugins(s, settings=Settings())
assert ret == expected