Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ Semantic versioning in our case means:
But, in the future we might change the configuration names/logic,
change the client facing API, change code conventions significantly, etc.

## Unreleased

### Features

- Adds `WPS367`: forbid redundant trailing colon in slice, #1071
- Adds `WPS482`: forbid initializing multiple variables on one line, #1901

## 1.6.2

### Bugfixes
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import pytest

from wemake_python_styleguide.violations.best_practices import (
MultipleVariablesInitializationViolation,
)
from wemake_python_styleguide.visitors.ast.builtins import (
MultipleVariablesInitializationVisitor,
)

# Correct usages:

function_unpacking = 'first, second = some_tuple()'
swap_assignment = 'first, second = second, first'
single_assignment = 'constant = 1'
spread_assignment = 'first, *_, second = [1, 2, 4, 3]'

# Wrong usages:

tuple_assignment = 'first, second = (1, 2)'
list_unpacking = 'first, second = [], []'
mixed_unpacking = 'first, second = (1, [])'


@pytest.mark.parametrize(
'code',
[
single_assignment,
spread_assignment,
function_unpacking,
swap_assignment,
],
)
def test_correct_assignments(
assert_errors,
parse_ast_tree,
code,
default_options,
):
"""Testing that correct assignments work."""
tree = parse_ast_tree(code)

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

assert_errors(visitor, [])


@pytest.mark.parametrize(
'code',
[
tuple_assignment,
list_unpacking,
mixed_unpacking,
],
)
def test_multiple_variables_initialization(
assert_errors,
parse_ast_tree,
code,
default_options,
):
"""Testing that multiple variables initialization is restricted."""
tree = parse_ast_tree(code)

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

assert_errors(visitor, [MultipleVariablesInitializationViolation])
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
'3:None:2',
'3:7:None',
'3:7:1',
'::1',
'1::1',
],
)
def test_one_redundant_subscript(
Expand Down Expand Up @@ -44,6 +46,7 @@ def test_one_redundant_subscript(
'None:None',
'3:None:1',
':None:None',
'0::1',
],
)
def test_two_redundant_subscript(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import pytest

from wemake_python_styleguide.violations.consistency import (
RedundantTrailingSliceViolation,
)
from wemake_python_styleguide.visitors.tokenize.subscripts import (
RedundantTrailingSliceVisitor,
)

# Wrong:
trailing_colon_cases = [
'a[1:4:]',
'a[1::]',
'a[:4:]',
'a[None::]',
'a[1:None:]',
'a[1 + 2::]',
]

# Correct:
correct_cases = [
'a[1:4]',
'a[1:]',
'a[:4]',
'a[::]', # caught by NonStrictSliceOperationsViolation
'a[1:4:5]',
'a[1:4:None]',
'a[1]',
'a[1:4:1]', # caught by RedundantSubscriptViolation, not ours
'a[b[1:4:5]]', # nested valid slice
'a[b[1:4:]]', # nested invalid — should flag inner
'a[{1: 2}]', # dict literal inside subscript
'a[(1, 2)]', # tuple inside subscript
'a[lambda x: x]', # lambda inside subscript
]


@pytest.mark.parametrize('code', trailing_colon_cases)
def test_redundant_trailing_colon(
parse_tokens,
assert_errors,
default_options,
code,
):
"""Ensure trailing colon in slice is forbidden."""
file_tokens = parse_tokens(code)
visitor = RedundantTrailingSliceVisitor(
default_options,
file_tokens=file_tokens,
)
visitor.run()
assert_errors(visitor, [RedundantTrailingSliceViolation])


@pytest.mark.parametrize('code', correct_cases)
def test_correct_slice_not_flagged(
parse_tokens,
assert_errors,
default_options,
code,
):
"""Ensure valid slices do not raise violation."""
file_tokens = parse_tokens(code)
visitor = RedundantTrailingSliceVisitor(
default_options,
file_tokens=file_tokens,
)
visitor.run()
assert_errors(visitor, [])
2 changes: 2 additions & 0 deletions wemake_python_styleguide/presets/types/file_tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
conditions,
primitives,
statements,
subscripts,
syntax,
)

Expand All @@ -21,4 +22,5 @@
statements.MultilineStringVisitor,
conditions.IfElseVisitor,
primitives.MultilineFormattedStringTokenVisitor,
subscripts.RedundantTrailingSliceVisitor,
)
1 change: 1 addition & 0 deletions wemake_python_styleguide/presets/types/tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
builtins.WrongStringVisitor,
builtins.WrongFormatStringVisitor,
builtins.WrongAssignmentVisitor,
builtins.MultipleVariablesInitializationVisitor,
builtins.WrongCollectionVisitor,
operators.UselessOperatorsVisitor,
operators.WrongMathOperatorVisitor,
Expand Down
32 changes: 32 additions & 0 deletions wemake_python_styleguide/violations/best_practices.py
Original file line number Diff line number Diff line change
Expand Up @@ -3019,3 +3019,35 @@ class Some:

error_template = 'Found a leaking ``for`` loop in a class or module body'
code = 481


@final
class MultipleVariablesInitializationViolation(ASTViolation):
"""
Forbid initializing multiple variables on a single line.

Reasoning:
Assigning multiple variables in one line reduces readability.
It is harder to see where each variable is initialized.

Solution:
Use separate assignment statements for each variable.

Example::

# Correct:
ab = []
cd = []
a, b = some_tuple()
x, y = y, x

# Wrong:
ab, cd = [], []
x, y = (1, 2)

.. versionadded:: 1.7.0

"""

error_template = 'Found multiple variables initialized on one line'
code = 482
33 changes: 33 additions & 0 deletions wemake_python_styleguide/violations/consistency.py
Original file line number Diff line number Diff line change
Expand Up @@ -2511,3 +2511,36 @@ class MeaninglessBooleanOperationViolation(ASTViolation):

error_template = 'Found meaningless boolean operation'
code = 366


@final
class RedundantTrailingSliceViolation(TokenizeViolation):
"""
Forbid redundant trailing colon in subscript slice.

Reasoning:
Trailing colon inside a subscript slice does not change behavior.
For example, ``a[1:4:]`` is parsed identically to ``a[1:4]``.
It adds visual noise for no reason.

Solution:
Remove the trailing colon.

Example::

# Correct:
a[1:4]
a[1:]
a[:4]

# Wrong:
a[1:4:]
a[1::]
a[:4:]

.. versionadded:: 1.7.0

"""

error_template = 'Found redundant trailing slice colon'
code = 367
28 changes: 28 additions & 0 deletions wemake_python_styleguide/visitors/ast/builtins.py
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,34 @@ def _check_unpacking_target_types(self, node: ast.AST | None) -> None:
)


@final
class MultipleVariablesInitializationVisitor(base.BaseNodeVisitor):
"""Ensures that multiple variables are not initialized on one line."""

def visit_Assign(self, node: ast.Assign) -> None:
"""Checks assignments for multiple variables initialization."""
self._check_multiple_variables_initialization(node)
self.generic_visit(node)

def _check_multiple_variables_initialization(
self,
node: ast.Assign,
) -> None:
if len(node.targets) != 1:
return
target = node.targets[0]
if not isinstance(target, ast.Tuple):
return
if not isinstance(node.value, ast.Tuple):
return
# Allow swapping: x, y = y, x (all elements in value are ast.Name)
if all(isinstance(elt, ast.Name) for elt in node.value.elts):
return
self.add_violation(
best_practices.MultipleVariablesInitializationViolation(node),
)


@final
class WrongCollectionVisitor(base.BaseNodeVisitor):
"""Ensures that collection definitions are correct."""
Expand Down
Loading