Skip to content

Commit

Permalink
Allows to reuse variables, closes #793, closes #812
Browse files Browse the repository at this point in the history
  • Loading branch information
sobolevn committed Sep 19, 2019
1 parent c0b5c9c commit 1b75d1a
Show file tree
Hide file tree
Showing 6 changed files with 218 additions and 7 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ We used to have incremental versioning before `0.1.0`.

### Bugfixes

- We now ignore `@overload` from `BlockAndLocalOverlapViolation`
- Now expressions that reuse block variables are not treated as violations,
example: `my_var = do_some(my_var)`

### Misc

- Adds Github Action and docs how to use it
- Adds local Github Action that uses itself for testing
- Adds official Docker image and docs about it


## 0.12.4
Expand Down
48 changes: 48 additions & 0 deletions docs/pages/usage/integrations/github-actions.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
Github Actions
--------------

.. image:: https://github.com/wemake-services/wemake-python-styleguide/workflows/wps/badge.svg
:alt: Github Action badge
:target: https://github.com/wemake-services/wemake-python-styleguide/actions

Good news: we ship pre-built Github Action with this project.

You can use it from the Github Marketplace:

.. code:: yaml
- name: wemake-python-styleguide
uses: wemake-python-styleguide@latest
You can also specify any version
starting from ``0.12.5`` instead of the ``latest`` tag.

Inputs
~~~~~~

We also support custom path to be specified:

.. code:: yaml
- name: wemake-python-styleguide
uses: wemake-python-styleguide@latest
with:
path: './your/custom/path'
Outputs
~~~~~~~

We also support ``outputs`` from the spec, so you can later
pass the output of ``wemake-python-styleguide`` to somewhere else.
For example to `reviewdog <https://github.com/reviewdog/reviewdog>`_ app.

.. code:: yaml
- name: wemake-python-styleguide
uses: wemake-python-styleguide@latest
with:
path: './your/custom/path'
- name: Custom reviewdog Action
runs: echo "{{ steps.wemake-python-styleguide.outputs.output }}" | reviewdog -f pep8
Note, that Github Actions are currently in beta.
1 change: 1 addition & 0 deletions docs/pages/usage/integrations/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
legacy.rst
flakehell.rst
docker.rst
github-actions.rst
ci.rst
stubs.rst
pylint.rst
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# -*- coding: utf-8 -*-

import pytest

from wemake_python_styleguide.violations.best_practices import (
BlockAndLocalOverlapViolation,
)
from wemake_python_styleguide.visitors.ast.blocks import BlockVariableVisitor

# Contexts:
context = """
{0}
{1}
"""

# Block statements:
block_statement1 = 'from some import {0}, {1}'


@pytest.mark.parametrize('block_statement', [
block_statement1,
])
@pytest.mark.parametrize('local_statement', [
'{0} = func({0})',
'{0} = {0}(arg)',
'{0} = {0} + 1',
'{0} = {0}.attr',
'{0} = {0}["key"]',
'{0} = d[{0}]',
'{0} = d[{0}:1]',
'{0}: type = func({0})',
'{0}: type = {0}(arg)',
'{0}: type = {0} + 1',
'{0}: type = {0}.attr',
'{0}: type = {0}["key"]',
'{0}: type = d[{0}]',
'{0}, {1} = {0}({1})',
'{0}, {1} = {0} * {1}',
'{1}, {0} = ({1}, {0})',
'{1}, *{0} = ({1}, {0})',
])
@pytest.mark.parametrize(('first_name', 'second_name'), [
('no_raise', 'used'),
])
def test_reuse_no_overlap(
assert_errors,
parse_ast_tree,
default_options,
mode,
block_statement,
local_statement,
first_name,
second_name,
):
"""Ensures that overlaping variables does not exist."""
code = context.format(
block_statement.format(first_name, second_name),
local_statement.format(first_name, second_name),
)
tree = parse_ast_tree(mode(code))

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

assert_errors(visitor, [])


@pytest.mark.parametrize('block_statement', [
block_statement1,
])
@pytest.mark.parametrize('local_statement', [
'{0} = func({1})',
'{0} = {1}(arg)',
'{0} = {1} + 1',
'{0} = {1}.attr',
'{0} = {1}["key"]',
'{0} = d[{1}]',
'{0} = d[{1}:1]',
'{0}: {1}',
'{0}: type = func({1})',
'{0}: type = {1}(arg)',
'{0}: type = {1} + 1',
'{0}: type = {1}.attr',
'{0}: type = {1}["key"]',
'{0}: type = d[{1}]',
'{0}, {1} = {0}({0})',
'{0}, {1} = {1} * {1}',
'{1}, {0} = ({0}, {0})',
'{1}, *{0} = ({1}, {1})',
])
@pytest.mark.parametrize(('first_name', 'second_name'), [
('no_raise', 'used'),
])
def test_reuse_overlap(
assert_errors,
parse_ast_tree,
default_options,
mode,
block_statement,
local_statement,
first_name,
second_name,
):
"""Ensures that overlaping variables exist no."""
code = context.format(
block_statement.format(first_name, second_name),
local_statement.format(first_name, second_name),
)
tree = parse_ast_tree(mode(code))

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

assert_errors(visitor, [BlockAndLocalOverlapViolation])
21 changes: 17 additions & 4 deletions wemake_python_styleguide/logic/scopes.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from typing_extensions import final

from wemake_python_styleguide.compat.aliases import FunctionNodes
from wemake_python_styleguide.compat.aliases import AssignNodes, FunctionNodes
from wemake_python_styleguide.logic.naming import access, name_nodes
from wemake_python_styleguide.logic.nodes import get_context
from wemake_python_styleguide.logic.source import node_to_string
Expand All @@ -15,6 +15,9 @@
#: That's how we represent scopes that are bound to contexts.
_ContextStore = DefaultDict[ContextNodes, Set[str]]

#: That's what we expect from `@overload` decorator:
_overload_exceptions = frozenset(('overload', 'typing.overload'))


class _BaseScope(object):
"""Base class for scope operations."""
Expand Down Expand Up @@ -133,13 +136,23 @@ def extract_names(node: ast.AST) -> Set[str]:
return set(name_nodes.get_variables_from_node(node))


_overload_exceptions = frozenset(('overload', 'typing.overload'))


def is_function_overload(node: ast.AST) -> bool:
"""Check that function decorated with `typing.overload`."""
if isinstance(node, FunctionNodes):
for decorator in node.decorator_list:
if node_to_string(decorator) in _overload_exceptions:
return True
return False


def is_same_value_reuse(node: ast.AST, names: Set[str]) -> bool:
"""Checks if the given names are reused by the given node."""
if isinstance(node, AssignNodes) and node.value:
used_names = {
name_node.id
for name_node in ast.walk(node.value)
if isinstance(name_node, ast.Name)
}
if not names.difference(used_names):
return True
return False
38 changes: 35 additions & 3 deletions wemake_python_styleguide/visitors/ast/blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,16 @@

import ast
from collections import defaultdict
from typing import ClassVar, DefaultDict, List, Set, Union, cast
from typing import (
Callable,
ClassVar,
DefaultDict,
List,
Set,
Tuple,
Union,
cast,
)

from typing_extensions import final

Expand All @@ -16,6 +25,7 @@
OuterScope,
extract_names,
is_function_overload,
is_same_value_reuse,
)
from wemake_python_styleguide.logic.walk import is_contained_by
from wemake_python_styleguide.types import (
Expand All @@ -39,6 +49,10 @@
DefaultDict[str, List[ast.AST]],
]

#: That's how we filter some overlaps that do happen in Python:
_ScopePredicate = Callable[[ast.AST, Set[str]], bool]
_NamePredicate = Callable[[ast.AST], bool]


@final
@decorators.alias('visit_named_nodes', (
Expand Down Expand Up @@ -74,6 +88,14 @@ class BlockVariableVisitor(base.BaseNodeVisitor):
"""

_naming_predicates: Tuple[_NamePredicate, ...] = (
is_function_overload,
)

_scope_predicates: Tuple[_ScopePredicate, ...] = (
is_same_value_reuse,
)

# Blocks:

def visit_named_nodes(self, node: AnyFunctionDef) -> None:
Expand Down Expand Up @@ -162,11 +184,21 @@ def _scope(
scope = BlockScope(node)
shadow = scope.shadowing(names, is_local=is_local)

if shadow:
ignored_scope = any(
predicate(node, names)
for predicate in self._scope_predicates
)
ignored_name = any(
predicate(node)
for predicate in self._naming_predicates
)

if shadow and not ignored_scope:
self.add_violation(
BlockAndLocalOverlapViolation(node, text=', '.join(shadow)),
)
if not is_function_overload(node):

if not ignored_name:
scope.add_to_scope(names, is_local=is_local)

def _outer_scope(self, node: ast.AST, names: Set[str]) -> None:
Expand Down

0 comments on commit 1b75d1a

Please sign in to comment.