Skip to content

Commit

Permalink
Adds new rules about unpacking, closes #405, closes #397
Browse files Browse the repository at this point in the history
  • Loading branch information
sobolevn committed Dec 18, 2018
1 parent 2f57cc7 commit cb3df77
Show file tree
Hide file tree
Showing 9 changed files with 219 additions and 4 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ We used to have incremental versioning before `0.1.0`.
- Forbids to use multi-line function type annotations
- Forbids to use uppercase string modifiers
- Forbids to use assign chains: now we only can use one assign per line
- Forbids to use assign with unpacking for any nodes except `Name`
- Forbids to have duplicate `except` blocks

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

### Misc

Expand Down
4 changes: 3 additions & 1 deletion tests/fixtures/noqa.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,8 +252,10 @@ async def function_with_unreachable():

first = second = 2 # noqa: Z445

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

try: # noqa: Z446

try: # noqa: Z447
anti_z444 = 1
except ValueError:
anti_z444 = 1
Expand Down
1 change: 1 addition & 0 deletions tests/test_checker/test_noqa.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ def test_noqa_fixture_disabled(absolute_path, all_violations):
'Z444': 2,
'Z445': 1,
'Z446': 1,
'Z447': 1,
}

process = subprocess.Popen(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# -*- coding: utf-8 -*-

import pytest

from wemake_python_styleguide.violations.best_practices import (
IncorrectUnpackingViolation,
)
from wemake_python_styleguide.visitors.ast.builtins import (
WrongAssignmentVisitor,
)

single_assignment = '{0} = 1'

tuple_assignment1 = 'first, {0} = (1, 2)'
tuple_assignment1 = '{0}, second = (1, 2)'

spread_assignment1 = '{0}, *second = [1, 2, 3]'
spread_assignment2 = 'first, *{0} = [1, 2, 3]'

for_assignment = """
for {0} in []:
...
"""

for_unpacking1 = """
for {0}, second in enumerate([]):
...
"""

for_unpacking2 = """
for first, {0} in enumerate([]):
...
"""

with_assignment = """
with some() as {0}:
...
"""

with_unpacking1 = """
with some() as ({0}, second):
...
"""

with_unpacking2 = """
with some() as (first, {0}):
...
"""


@pytest.mark.parametrize('code', [
single_assignment,
tuple_assignment1,
tuple_assignment1,
spread_assignment1,
spread_assignment2,
for_assignment,
for_unpacking1,
for_unpacking2,
with_assignment,
with_unpacking1,
with_unpacking2,
])
def test_correct_unpacking(
assert_errors,
parse_ast_tree,
code,
default_options,
):
"""Testing that correct assignments work."""
tree = parse_ast_tree(code.format('some_name'))

visitor = WrongAssignmentVisitor(default_options, tree=tree)
visitor.run()

assert_errors(visitor, [])


@pytest.mark.parametrize('assignment', [
'some[0]',
'some["key"]',
'some.attr',
])
def test_correct_assignment(
assert_errors,
parse_ast_tree,
assignment,
default_options,
):
"""Testing that correct assignments work."""
tree = parse_ast_tree(single_assignment.format(assignment))

visitor = WrongAssignmentVisitor(default_options, tree=tree)
visitor.run()

assert_errors(visitor, [])


@pytest.mark.parametrize('code', [
tuple_assignment1,
tuple_assignment1,
spread_assignment1,
spread_assignment2,
for_unpacking1,
for_unpacking2,
with_unpacking1,
with_unpacking2,
])
@pytest.mark.parametrize('assignment', [
'some[0]',
'some["key"]',
'some.attr',
])
def test_multiple_assignments(
assert_errors,
parse_ast_tree,
code,
assignment,
default_options,
):
"""Testing that multiple assignments are restricted."""
tree = parse_ast_tree(code.format(assignment))

visitor = WrongAssignmentVisitor(default_options, tree=tree)
visitor.run()

assert_errors(visitor, [IncorrectUnpackingViolation])
6 changes: 6 additions & 0 deletions tests/test_visitors/test_ast/test_naming/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@ def __init__(self):
{0} = 'test'
"""

# See: https://github.com/wemake-services/wemake-python-styleguide/issues/405
unpacking_variables = """
first.attr, {0} = range(2)
"""

for_variable = """
def container():
for {0} in []:
Expand Down Expand Up @@ -131,6 +136,7 @@ def container():
# Variables:
variable_def,
unpacking_variables,
for_variable,
with_variable,
exception,
Expand Down
36 changes: 35 additions & 1 deletion wemake_python_styleguide/violations/best_practices.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
UnreachableCodeViolation
StatementHasNoEffectViolation
MultipleAssignmentsViolation
IncorrectUnpackingViolation
DuplicateExceptionViolation
Comments
Expand Down Expand Up @@ -89,6 +90,7 @@
.. autoclass:: UnreachableCodeViolation
.. autoclass:: StatementHasNoEffectViolation
.. autoclass:: MultipleAssignmentsViolation
.. autoclass:: IncorrectUnpackingViolation
.. autoclass:: DuplicateExceptionViolation
"""
Expand Down Expand Up @@ -915,6 +917,7 @@ class LambdaInsideLoopViolation(ASTViolation):
code = 442


@final
class UnreachableCodeViolation(ASTViolation):
"""
Forbids to have unreachable code.
Expand Down Expand Up @@ -958,6 +961,7 @@ def some_function():
code = 443


@final
class StatementHasNoEffectViolation(ASTViolation):
"""
Forbids to have statements that do nothing.
Expand Down Expand Up @@ -991,6 +995,7 @@ def some_function():
code = 444


@final
class MultipleAssignmentsViolation(ASTViolation):
"""
Forbids to have statements that do nothing.
Expand Down Expand Up @@ -1020,6 +1025,35 @@ class MultipleAssignmentsViolation(ASTViolation):
code = 445


@final
class IncorrectUnpackingViolation(ASTViolation):
"""
Forbids to have statements that do nothing.
Reasoning:
Having unpacking with side-effects is very dirty.
You might get in serious and very hard-to-debug troubles because of
this technique. So, do not use it.
Solution:
Use unpacking with only variables, not any other entities.
Example::
# Correct:
first, second = some()
# Wrong:
first, some_dict['alias'] = some()
.. versionadded:: 0.6.0
"""

error_template = 'Found incorrect unpacking target'
code = 446


@final
class DuplicateExceptionViolation(ASTViolation):
"""
Expand Down Expand Up @@ -1054,4 +1088,4 @@ class DuplicateExceptionViolation(ASTViolation):
"""

error_template = 'Found duplicate exception: {0}'
code = 446
code = 447
46 changes: 44 additions & 2 deletions wemake_python_styleguide/visitors/ast/builtins.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
# -*- coding: utf-8 -*-

import ast
from typing import ClassVar, Optional
from typing import ClassVar, Iterable, Optional

from wemake_python_styleguide import constants
from wemake_python_styleguide.types import AnyNodes, final
from wemake_python_styleguide.violations.best_practices import (
IncorrectUnpackingViolation,
MagicNumberViolation,
MultipleAssignmentsViolation,
)
Expand Down Expand Up @@ -102,13 +103,54 @@ def _check_assign_targets(self, node: ast.Assign) -> None:
if len(node.targets) > 1:
self.add_violation(MultipleAssignmentsViolation(node))

def _check_unpacking_targets(
self,
node: ast.AST,
targets: Iterable[ast.AST],
) -> None:
for target in targets:
if isinstance(target, ast.Starred):
target = target.value
if not isinstance(target, ast.Name):
self.add_violation(IncorrectUnpackingViolation(node))

def visit_With(self, node: ast.With) -> None:
"""
Checks assignments inside context managers to be correct.
Raises:
IncorrectUnpackingViolation
"""
for withitem in node.items:
if isinstance(withitem.optional_vars, ast.Tuple):
self._check_unpacking_targets(
node, withitem.optional_vars.elts,
)
self.generic_visit(node)

def visit_For(self, node: ast.For) -> None:
"""
Checks assignments inside ``for`` loops to be correct.
Raises:
IncorrectUnpackingViolation
"""
if isinstance(node.target, ast.Tuple):
self._check_unpacking_targets(node, node.target.elts)
self.generic_visit(node)

def visit_Assign(self, node: ast.Assign) -> None:
"""
Checks assingments to be correct.
Checks assignments to be correct.
Raises:
MultipleAssignmentsViolation
IncorrectUnpackingViolation
"""
self._check_assign_targets(node)
if isinstance(node.targets[0], ast.Tuple):
self._check_unpacking_targets(node, node.targets[0].elts)
self.generic_visit(node)
1 change: 1 addition & 0 deletions wemake_python_styleguide/visitors/ast/naming.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ def _create_target_names(
for index, _ in enumerate(target_names):
target_names[index] = tuple(
name.id for name in target_names[index]
if isinstance(name, ast.Name)
)
return target_names

Expand Down

0 comments on commit cb3df77

Please sign in to comment.