Skip to content

Commit cb3df77

Browse files
committed
Adds new rules about unpacking, closes #405, closes #397
1 parent 2f57cc7 commit cb3df77

File tree

9 files changed

+219
-4
lines changed

9 files changed

+219
-4
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ We used to have incremental versioning before `0.1.0`.
1919
- Forbids to use multi-line function type annotations
2020
- Forbids to use uppercase string modifiers
2121
- Forbids to use assign chains: now we only can use one assign per line
22+
- Forbids to use assign with unpacking for any nodes except `Name`
2223
- Forbids to have duplicate `except` blocks
2324

2425
### Bugfixes
@@ -30,6 +31,7 @@ We used to have incremental versioning before `0.1.0`.
3031
- Fixes bug when it was possible to provide non-unique aliases
3132
- Fixes incorrect line number for incorrect parameter names
3233
- Fixes bug when names like `__some__value__` were not treated as underscored
34+
- Fixes bug when assignment to anything rather than name was raising an error
3335

3436
### Misc
3537

tests/fixtures/noqa.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,8 +252,10 @@ async def function_with_unreachable():
252252

253253
first = second = 2 # noqa: Z445
254254

255+
index, nodes[0] = range(2) # noqa: Z446
255256

256-
try: # noqa: Z446
257+
258+
try: # noqa: Z447
257259
anti_z444 = 1
258260
except ValueError:
259261
anti_z444 = 1

tests/test_checker/test_noqa.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ def test_noqa_fixture_disabled(absolute_path, all_violations):
115115
'Z444': 2,
116116
'Z445': 1,
117117
'Z446': 1,
118+
'Z447': 1,
118119
}
119120

120121
process = subprocess.Popen(
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
# -*- coding: utf-8 -*-
2+
3+
import pytest
4+
5+
from wemake_python_styleguide.violations.best_practices import (
6+
IncorrectUnpackingViolation,
7+
)
8+
from wemake_python_styleguide.visitors.ast.builtins import (
9+
WrongAssignmentVisitor,
10+
)
11+
12+
single_assignment = '{0} = 1'
13+
14+
tuple_assignment1 = 'first, {0} = (1, 2)'
15+
tuple_assignment1 = '{0}, second = (1, 2)'
16+
17+
spread_assignment1 = '{0}, *second = [1, 2, 3]'
18+
spread_assignment2 = 'first, *{0} = [1, 2, 3]'
19+
20+
for_assignment = """
21+
for {0} in []:
22+
...
23+
"""
24+
25+
for_unpacking1 = """
26+
for {0}, second in enumerate([]):
27+
...
28+
"""
29+
30+
for_unpacking2 = """
31+
for first, {0} in enumerate([]):
32+
...
33+
"""
34+
35+
with_assignment = """
36+
with some() as {0}:
37+
...
38+
"""
39+
40+
with_unpacking1 = """
41+
with some() as ({0}, second):
42+
...
43+
"""
44+
45+
with_unpacking2 = """
46+
with some() as (first, {0}):
47+
...
48+
"""
49+
50+
51+
@pytest.mark.parametrize('code', [
52+
single_assignment,
53+
tuple_assignment1,
54+
tuple_assignment1,
55+
spread_assignment1,
56+
spread_assignment2,
57+
for_assignment,
58+
for_unpacking1,
59+
for_unpacking2,
60+
with_assignment,
61+
with_unpacking1,
62+
with_unpacking2,
63+
])
64+
def test_correct_unpacking(
65+
assert_errors,
66+
parse_ast_tree,
67+
code,
68+
default_options,
69+
):
70+
"""Testing that correct assignments work."""
71+
tree = parse_ast_tree(code.format('some_name'))
72+
73+
visitor = WrongAssignmentVisitor(default_options, tree=tree)
74+
visitor.run()
75+
76+
assert_errors(visitor, [])
77+
78+
79+
@pytest.mark.parametrize('assignment', [
80+
'some[0]',
81+
'some["key"]',
82+
'some.attr',
83+
])
84+
def test_correct_assignment(
85+
assert_errors,
86+
parse_ast_tree,
87+
assignment,
88+
default_options,
89+
):
90+
"""Testing that correct assignments work."""
91+
tree = parse_ast_tree(single_assignment.format(assignment))
92+
93+
visitor = WrongAssignmentVisitor(default_options, tree=tree)
94+
visitor.run()
95+
96+
assert_errors(visitor, [])
97+
98+
99+
@pytest.mark.parametrize('code', [
100+
tuple_assignment1,
101+
tuple_assignment1,
102+
spread_assignment1,
103+
spread_assignment2,
104+
for_unpacking1,
105+
for_unpacking2,
106+
with_unpacking1,
107+
with_unpacking2,
108+
])
109+
@pytest.mark.parametrize('assignment', [
110+
'some[0]',
111+
'some["key"]',
112+
'some.attr',
113+
])
114+
def test_multiple_assignments(
115+
assert_errors,
116+
parse_ast_tree,
117+
code,
118+
assignment,
119+
default_options,
120+
):
121+
"""Testing that multiple assignments are restricted."""
122+
tree = parse_ast_tree(code.format(assignment))
123+
124+
visitor = WrongAssignmentVisitor(default_options, tree=tree)
125+
visitor.run()
126+
127+
assert_errors(visitor, [IncorrectUnpackingViolation])

tests/test_visitors/test_ast/test_naming/conftest.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,11 @@ def __init__(self):
8181
{0} = 'test'
8282
"""
8383

84+
# See: https://github.com/wemake-services/wemake-python-styleguide/issues/405
85+
unpacking_variables = """
86+
first.attr, {0} = range(2)
87+
"""
88+
8489
for_variable = """
8590
def container():
8691
for {0} in []:
@@ -131,6 +136,7 @@ def container():
131136
132137
# Variables:
133138
variable_def,
139+
unpacking_variables,
134140
for_variable,
135141
with_variable,
136142
exception,

wemake_python_styleguide/violations/best_practices.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
UnreachableCodeViolation
4747
StatementHasNoEffectViolation
4848
MultipleAssignmentsViolation
49+
IncorrectUnpackingViolation
4950
DuplicateExceptionViolation
5051
5152
Comments
@@ -89,6 +90,7 @@
8990
.. autoclass:: UnreachableCodeViolation
9091
.. autoclass:: StatementHasNoEffectViolation
9192
.. autoclass:: MultipleAssignmentsViolation
93+
.. autoclass:: IncorrectUnpackingViolation
9294
.. autoclass:: DuplicateExceptionViolation
9395
9496
"""
@@ -915,6 +917,7 @@ class LambdaInsideLoopViolation(ASTViolation):
915917
code = 442
916918

917919

920+
@final
918921
class UnreachableCodeViolation(ASTViolation):
919922
"""
920923
Forbids to have unreachable code.
@@ -958,6 +961,7 @@ def some_function():
958961
code = 443
959962

960963

964+
@final
961965
class StatementHasNoEffectViolation(ASTViolation):
962966
"""
963967
Forbids to have statements that do nothing.
@@ -991,6 +995,7 @@ def some_function():
991995
code = 444
992996

993997

998+
@final
994999
class MultipleAssignmentsViolation(ASTViolation):
9951000
"""
9961001
Forbids to have statements that do nothing.
@@ -1020,6 +1025,35 @@ class MultipleAssignmentsViolation(ASTViolation):
10201025
code = 445
10211026

10221027

1028+
@final
1029+
class IncorrectUnpackingViolation(ASTViolation):
1030+
"""
1031+
Forbids to have statements that do nothing.
1032+
1033+
Reasoning:
1034+
Having unpacking with side-effects is very dirty.
1035+
You might get in serious and very hard-to-debug troubles because of
1036+
this technique. So, do not use it.
1037+
1038+
Solution:
1039+
Use unpacking with only variables, not any other entities.
1040+
1041+
Example::
1042+
1043+
# Correct:
1044+
first, second = some()
1045+
1046+
# Wrong:
1047+
first, some_dict['alias'] = some()
1048+
1049+
.. versionadded:: 0.6.0
1050+
1051+
"""
1052+
1053+
error_template = 'Found incorrect unpacking target'
1054+
code = 446
1055+
1056+
10231057
@final
10241058
class DuplicateExceptionViolation(ASTViolation):
10251059
"""
@@ -1054,4 +1088,4 @@ class DuplicateExceptionViolation(ASTViolation):
10541088
"""
10551089

10561090
error_template = 'Found duplicate exception: {0}'
1057-
code = 446
1091+
code = 447

wemake_python_styleguide/visitors/ast/builtins.py

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
# -*- coding: utf-8 -*-
22

33
import ast
4-
from typing import ClassVar, Optional
4+
from typing import ClassVar, Iterable, Optional
55

66
from wemake_python_styleguide import constants
77
from wemake_python_styleguide.types import AnyNodes, final
88
from wemake_python_styleguide.violations.best_practices import (
9+
IncorrectUnpackingViolation,
910
MagicNumberViolation,
1011
MultipleAssignmentsViolation,
1112
)
@@ -102,13 +103,54 @@ def _check_assign_targets(self, node: ast.Assign) -> None:
102103
if len(node.targets) > 1:
103104
self.add_violation(MultipleAssignmentsViolation(node))
104105

106+
def _check_unpacking_targets(
107+
self,
108+
node: ast.AST,
109+
targets: Iterable[ast.AST],
110+
) -> None:
111+
for target in targets:
112+
if isinstance(target, ast.Starred):
113+
target = target.value
114+
if not isinstance(target, ast.Name):
115+
self.add_violation(IncorrectUnpackingViolation(node))
116+
117+
def visit_With(self, node: ast.With) -> None:
118+
"""
119+
Checks assignments inside context managers to be correct.
120+
121+
Raises:
122+
IncorrectUnpackingViolation
123+
124+
"""
125+
for withitem in node.items:
126+
if isinstance(withitem.optional_vars, ast.Tuple):
127+
self._check_unpacking_targets(
128+
node, withitem.optional_vars.elts,
129+
)
130+
self.generic_visit(node)
131+
132+
def visit_For(self, node: ast.For) -> None:
133+
"""
134+
Checks assignments inside ``for`` loops to be correct.
135+
136+
Raises:
137+
IncorrectUnpackingViolation
138+
139+
"""
140+
if isinstance(node.target, ast.Tuple):
141+
self._check_unpacking_targets(node, node.target.elts)
142+
self.generic_visit(node)
143+
105144
def visit_Assign(self, node: ast.Assign) -> None:
106145
"""
107-
Checks assingments to be correct.
146+
Checks assignments to be correct.
108147
109148
Raises:
110149
MultipleAssignmentsViolation
150+
IncorrectUnpackingViolation
111151
112152
"""
113153
self._check_assign_targets(node)
154+
if isinstance(node.targets[0], ast.Tuple):
155+
self._check_unpacking_targets(node, node.targets[0].elts)
114156
self.generic_visit(node)

wemake_python_styleguide/visitors/ast/naming.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ def _create_target_names(
267267
for index, _ in enumerate(target_names):
268268
target_names[index] = tuple(
269269
name.id for name in target_names[index]
270+
if isinstance(name, ast.Name)
270271
)
271272
return target_names
272273

0 commit comments

Comments
 (0)