Skip to content

Commit 9c00438

Browse files
committed
Add new fixer for 'not A is B' -> 'A is not B' situation.
Closes #110. Thanks to @rs2 for submitting the idea!
1 parent bd277ee commit 9c00438

File tree

6 files changed

+118
-1
lines changed

6 files changed

+118
-1
lines changed

README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,6 @@ def f():
161161

162162
if a or b or c:
163163
pass
164-
165164
```
166165

167166
* **B007: Convert f-strings without expressions into regular strings.**
@@ -214,7 +213,20 @@ def f():
214213
c = frozenset([(1, 2), ("a", "b")])
215214
```
216215

216+
* **B010: Replace 'not A is B' with 'A is not B'**
217+
218+
Usage of `A is not B` over `not A is B` is recommended both by Google and [PEP-8](https://www.python.org/dev/peps/pep-0008/#programming-recommendations). Both of those forms are compiled to the same bytecode, but second form has some potential of confusion for the reader.
219+
(thanks to @rs2 for submitting this!).
217220

221+
```python
222+
# BEFORE:
223+
if not obj is Record:
224+
sys.exit(-1)
225+
226+
# AFTER:
227+
if obj is not Record:
228+
sys.exit(-1)
229+
```
218230

219231
**NB:** Each of the fixers can be disabled on per-line basis using [flake8's "noqa" comments](http://flake8.pycqa.org/en/3.1.1/user/ignoring-errors.html#in-line-ignoring-errors).
220232

pybetter/cli.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
FixTrivialFmtStringCreation,
1818
FixTrivialNestedWiths,
1919
FixUnhashableList,
20+
FixNotIsConditionOrder,
2021
)
2122
from pybetter.utils import resolve_paths, create_diff, prettify_time_interval
2223

@@ -30,6 +31,7 @@
3031
FixTrivialFmtStringCreation,
3132
FixTrivialNestedWiths,
3233
FixUnhashableList,
34+
FixNotIsConditionOrder,
3335
)
3436

3537

pybetter/improvements.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from pybetter.transformers.mutable_args import ArgEmptyInitTransformer
1313
from pybetter.transformers.nested_withs import NestedWithTransformer
1414
from pybetter.transformers.not_in import NotInConditionTransformer
15+
from pybetter.transformers.not_is import NotIsConditionTransformer
1516
from pybetter.transformers.parenthesized_return import RemoveParenthesesFromReturn
1617
from pybetter.transformers.unhashable_list import UnhashableListTransformer
1718

@@ -95,6 +96,13 @@ class FixUnhashableList(BaseImprovement):
9596
TRANSFORMER = UnhashableListTransformer
9697

9798

99+
class FixNotIsConditionOrder(BaseImprovement):
100+
NAME = "not_is"
101+
DESCRIPTION = "Replace 'not A is B' with 'A is not B'"
102+
CODE = "B010"
103+
TRANSFORMER = NotIsConditionTransformer
104+
105+
98106
__all__ = [
99107
"BaseImprovement",
100108
"FixBooleanEqualityChecks",
@@ -106,4 +114,5 @@ class FixUnhashableList(BaseImprovement):
106114
"FixTrivialFmtStringCreation",
107115
"FixTrivialNestedWiths",
108116
"FixUnhashableList",
117+
"FixNotIsConditionOrder",
109118
]

pybetter/transformers/not_is.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import libcst as cst
2+
import libcst.matchers as m
3+
4+
from pybetter.transformers.base import NoqaAwareTransformer
5+
6+
7+
class NotIsConditionTransformer(NoqaAwareTransformer):
8+
@m.leave(
9+
m.UnaryOperation(
10+
operator=m.Not(),
11+
expression=m.Comparison(comparisons=[m.ComparisonTarget(operator=m.Is())]),
12+
)
13+
)
14+
def replace_not_in_condition(
15+
self, _, updated_node: cst.UnaryOperation
16+
) -> cst.BaseExpression:
17+
comparison_node: cst.Comparison = cst.ensure_type(
18+
updated_node.expression, cst.Comparison
19+
)
20+
21+
# TODO: Implement support for multiple consecutive 'not ... in B',
22+
# even if it does not make any sense in practice.
23+
return cst.Comparison(
24+
left=comparison_node.left,
25+
lpar=updated_node.lpar,
26+
rpar=updated_node.rpar,
27+
comparisons=[
28+
comparison_node.comparisons[0].with_changes(operator=cst.IsNot())
29+
],
30+
)
31+
32+
33+
__all__ = ["NotIsConditionTransformer"]

tests/test_not_in.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
NO_CHANGES_MADE,
4747
)
4848

49+
4950
CONSECUTIVE_COMPARISONS_UNCHANGED = (
5051
"""
5152
if not (a in b in c in d):

tests/test_not_is.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import pytest
2+
3+
from pybetter.cli import process_file
4+
from pybetter.improvements import FixNotIsConditionOrder
5+
6+
NO_CHANGES_MADE = None
7+
8+
TRIVIAL_NOT_A_IS_B_CASE = (
9+
"""
10+
if not a is B:
11+
pass
12+
""",
13+
"""
14+
if a is not B:
15+
pass
16+
""",
17+
)
18+
19+
20+
MULTIPLE_NOT_A_IS_B_CASE = (
21+
"""
22+
if foo is not None and not bar is None:
23+
pass
24+
""",
25+
"""
26+
if foo is not None and bar is not None:
27+
pass
28+
""",
29+
)
30+
31+
32+
NESTED_NOT_A_IS_B_CASE = (
33+
"""
34+
if not foo is (not A is B):
35+
pass
36+
""",
37+
"""
38+
if foo is not (A is not B):
39+
pass
40+
""",
41+
)
42+
43+
44+
@pytest.mark.parametrize(
45+
"original,expected",
46+
[
47+
TRIVIAL_NOT_A_IS_B_CASE,
48+
MULTIPLE_NOT_A_IS_B_CASE,
49+
NESTED_NOT_A_IS_B_CASE,
50+
],
51+
ids=[
52+
"trivial 'not A is B' case",
53+
"multiple 'not A is B' in same expression",
54+
"nested 'not A is B' case",
55+
],
56+
)
57+
def test_not_in_transformation(original, expected):
58+
processed, _ = process_file(original.strip(), [FixNotIsConditionOrder])
59+
60+
assert processed.strip() == (expected or original).strip()

0 commit comments

Comments
 (0)