Skip to content

Commit f5b0f41

Browse files
authored
Issue 1011: Non strict operation with slice (#3373)
1 parent 47f7b54 commit f5b0f41

7 files changed

Lines changed: 219 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ Semantic versioning in our case means:
1616
But, in the future we might change the configuration names/logic,
1717
change the client facing API, change code conventions significantly, etc.
1818

19+
## 1.2.0 WIP
20+
21+
### Features
22+
23+
- Adds `WPS478`: forbids using non strict slice operations, #1011
24+
1925
## 1.1.1
2026

2127
### Bugfixes

tests/fixtures/noqa/noqa.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -752,3 +752,6 @@ def pos_only_problem(first_argpm=0, second_argpm=1, /): # noqa: WPS475
752752
async def test_await_in_loop():
753753
for _ in range(10):
754754
await test_function() # noqa: WPS476
755+
756+
757+
some_sequence = some_sequence[::-1] # noqa: WPS478

tests/test_checker/test_noqa.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@
242242
'WPS475': 1,
243243
'WPS476': 1,
244244
'WPS477': 0, # enabled only in python 3.13+
245+
'WPS478': 1,
245246
'WPS500': 1,
246247
'WPS501': 1,
247248
'WPS502': 0, # disabled since 1.0.0
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import pytest
2+
3+
from wemake_python_styleguide.violations.best_practices import (
4+
NonStrictSliceOperationsViolation,
5+
)
6+
from wemake_python_styleguide.visitors.ast.subscripts import (
7+
StrictSliceOperations,
8+
)
9+
10+
11+
@pytest.mark.parametrize(
12+
'expression',
13+
[
14+
# reverse
15+
'items[::-1]',
16+
'items[None::-1]',
17+
'items[:None:-1]',
18+
'items[None:None:-1]',
19+
'items[-1::-1]',
20+
'items[-1:None:-1]',
21+
# copy
22+
'items[:]',
23+
'items[None:]',
24+
'items[:None]',
25+
'items[None:None]',
26+
'items[::]',
27+
'items[None::]',
28+
'items[:None:]',
29+
'items[::None]',
30+
'items[None:None:]',
31+
'items[None::None]',
32+
'items[:None:None]',
33+
'items[None:None:None]',
34+
'items[::1]',
35+
'items[None::1]',
36+
'items[:None:1]',
37+
'items[None:None:1]',
38+
'items[0:]',
39+
'items[0:None]',
40+
'items[0::]',
41+
'items[0:None:]',
42+
'items[0::None]',
43+
'items[0:None:None]',
44+
'items[0::1]',
45+
'items[0:None:1]',
46+
],
47+
)
48+
def test_non_strict_slice_operation_bad(
49+
assert_errors,
50+
parse_ast_tree,
51+
expression,
52+
default_options,
53+
):
54+
"""Testing for using non strict slice operations."""
55+
tree = parse_ast_tree(expression)
56+
57+
visitor = StrictSliceOperations(default_options, tree=tree)
58+
visitor.run()
59+
60+
assert_errors(visitor, [NonStrictSliceOperationsViolation])
61+
62+
63+
@pytest.mark.parametrize(
64+
'expression',
65+
[
66+
'items[0]',
67+
'items[:x]',
68+
'items[:2]',
69+
'items[2:]',
70+
'items[:-1]',
71+
'items[2::]',
72+
'items[2:2]',
73+
'items[:-x]',
74+
'items[::x]',
75+
'items[::-x]',
76+
'items[0:-1]',
77+
'items[:-1:]',
78+
'items[2::2]',
79+
'items[1::-1]',
80+
'items[None:x]',
81+
'items[:None:x]',
82+
'items[None:-1]',
83+
'items[1:4:-1]',
84+
'items[None:-x]',
85+
'items[None::x]',
86+
'items[0:-1:1]',
87+
'items[:None:-x]',
88+
'items[None:-1:]',
89+
'items[:-1:None]',
90+
'items[2:None:2]',
91+
'items[0:-1:None]',
92+
'items[None::-x]',
93+
'items[1:None:-1]',
94+
'items[None:None:x]',
95+
'items[None:-1:None]',
96+
],
97+
)
98+
def test_non_strict_slice_operation_good(
99+
assert_errors,
100+
parse_ast_tree,
101+
expression,
102+
default_options,
103+
):
104+
"""Testing for using non strict slice operations."""
105+
tree = parse_ast_tree(expression)
106+
107+
visitor = StrictSliceOperations(default_options, tree=tree)
108+
visitor.run()
109+
110+
assert_errors(visitor, [])

wemake_python_styleguide/presets/types/tree.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
subscripts.SubscriptVisitor,
7575
subscripts.ImplicitDictGetVisitor,
7676
subscripts.CorrectKeyVisitor,
77+
subscripts.StrictSliceOperations,
7778
decorators.WrongDecoratorVisitor,
7879
redundancy.RedundantEnumerateVisitor,
7980
pm.MatchSubjectVisitor,

wemake_python_styleguide/violations/best_practices.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
ProblematicFunctionParamsViolation
9595
AwaitInLoopViolation
9696
SneakyTypeVarWithDefaultViolation
97+
NonStrictSliceOperationsViolation
9798
9899
Best practices
99100
--------------
@@ -176,6 +177,7 @@
176177
.. autoclass:: ProblematicFunctionParamsViolation
177178
.. autoclass:: AwaitInLoopViolation
178179
.. autoclass:: SneakyTypeVarWithDefaultViolation
180+
.. autoclass:: NonStrictSliceOperationsViolation
179181
180182
"""
181183

@@ -3042,3 +3044,33 @@ class Class[T=int]:
30423044

30433045
error_template = 'Found a TypeVarTuple following a TypeVar with default'
30443046
code = 477
3047+
3048+
3049+
@final
3050+
class NonStrictSliceOperationsViolation(ASTViolation):
3051+
"""
3052+
Forbid using non strict slice operations.
3053+
3054+
Reasoning:
3055+
We have two ways to do something.
3056+
3057+
Solution:
3058+
Prefer a more descriptive way.
3059+
3060+
Example::
3061+
3062+
# Correct:
3063+
items.reverse()
3064+
''.join(reversed('abc'))
3065+
items.copy()
3066+
3067+
# Wrong:
3068+
items[::-1] # `.reverse()` or `reversed()`
3069+
items[:] # `.copy()` or `copy.copy()`
3070+
3071+
.. versionadded:: 1.2.0
3072+
3073+
"""
3074+
3075+
error_template = 'Found non strict slice operation'
3076+
code = 478

wemake_python_styleguide/visitors/ast/subscripts.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,3 +182,69 @@ def _is_float_key(self, node: ast.expr) -> bool:
182182
return isinstance(real_node, ast.Constant) and isinstance(
183183
real_node.value, float
184184
)
185+
186+
187+
@final
188+
class StrictSliceOperations(base.BaseNodeVisitor):
189+
"""Check for stricter operation with slices."""
190+
191+
def visit_Slice(self, node: ast.Slice) -> None:
192+
"""Visit slice."""
193+
self._check_reverse(node)
194+
self._check_copy(node)
195+
self.generic_visit(node)
196+
197+
def _check_reverse(self, node: ast.Slice) -> None:
198+
if not (
199+
self._is_node_or_none(node.lower)
200+
or self._is_node_have_value(node.lower, value_to_check=-1)
201+
):
202+
return
203+
204+
if not (
205+
self._is_node_or_none(node.upper)
206+
and self._is_node_have_value(node.step, value_to_check=-1)
207+
):
208+
return
209+
210+
self.add_violation(
211+
best_practices.NonStrictSliceOperationsViolation(node)
212+
)
213+
214+
def _check_copy(self, node: ast.Slice) -> None:
215+
if not (
216+
self._is_node_or_none(node.lower)
217+
or self._is_node_have_value(node.lower, value_to_check=0)
218+
):
219+
return
220+
221+
if not self._is_node_or_none(node.upper):
222+
return
223+
224+
if not (
225+
self._is_node_or_none(node.step)
226+
or self._is_node_have_value(node.step, value_to_check=1)
227+
):
228+
return
229+
230+
self.add_violation(
231+
best_practices.NonStrictSliceOperationsViolation(node)
232+
)
233+
234+
def _is_node_or_none(self, node: ast.AST | None) -> bool:
235+
return node is None or (
236+
isinstance(node, ast.Constant) and node.value is None
237+
)
238+
239+
def _is_node_have_value(
240+
self, node: ast.AST | None, value_to_check: int
241+
) -> bool:
242+
if value_to_check < 0:
243+
return (
244+
isinstance(node, ast.UnaryOp)
245+
and isinstance(node.op, ast.USub)
246+
and isinstance(node.operand, ast.Constant)
247+
and node.operand.value == abs(value_to_check)
248+
)
249+
250+
return isinstance(node, ast.Constant) and node.value == value_to_check

0 commit comments

Comments
 (0)