Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add new domain names options #1106

Merged
merged 5 commits into from
Jan 26, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
29 changes: 29 additions & 0 deletions tests/test_checker/test_invalid_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,32 @@ def test_invalid_options(absolute_path):

assert process.returncode == 1
assert 'ValueError' in stderr


def test_invalid_domain_names_options(absolute_path):
"""End-to-End test to check domain names options validation works."""
process = subprocess.Popen(
[
'flake8',
'--isolated',
'--select',
'WPS',
# values from `allowed-domain-names` cannot intersect with
# `--forbidden-domain-names`
'--allowed-domain-names',
'item,items,handle,visitor',
'--forbidden-domain-names',
'handle,visitor,node',
absolute_path('fixtures', 'noqa.py'),
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True,
encoding='utf8',
)
_, stderr = process.communicate()

assert process.returncode == 1
assert 'ValueError' in stderr
assert 'handle' in stderr
assert 'visitor' in stderr
28 changes: 28 additions & 0 deletions tests/test_options/test_validate_domain_names_options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-

import pytest

from wemake_python_styleguide.options.validation import (
validate_domain_names_options,
)


@pytest.mark.parametrize(('allowed_names', 'forbidden_names'), [
(['items'], []),
([], ['items']),
(['item'], ['handle']),
])
def test_passes_when_any_option_not_passed(allowed_names, forbidden_names):
"""Ensures validation passes when any domain option not passed."""
validate_domain_names_options(allowed_names, forbidden_names)


def test_passes_when_names_no_intersect():
"""Ensures validation passes when names no intersect."""
validate_domain_names_options(['node'], ['visitor'])


def test_raises_valueerror_when_names_intersect():
"""Ensures `ValueError` exception is raised when names intersect."""
with pytest.raises(ValueError):
validate_domain_names_options(['visitor', 'handle'], ['visitor'])
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,28 @@ def test_naming_correct(
visitor.run()

assert_errors(visitor, [])


@pytest.mark.parametrize('allowed_name', [
'item',
'items',
'handle',
'other_name', # unknown values are ignored silently
])
def test_name_in_allowed_domain_names_option(
assert_errors,
parse_ast_tree,
naming_template,
options,
mode,
allowed_name,
):
"""Ensures that names listed in `allowed-domain-names` are allowed."""
tree = parse_ast_tree(mode(naming_template.format(allowed_name)))

visitor = WrongNameVisitor(
options(allowed_domain_names=[allowed_name]),
tree=tree,
)
visitor.run()
assert_errors(visitor, [])
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,29 @@ def test_wrong_variable_name(

assert_errors(visitor, [WrongVariableNameViolation])
assert_error_text(visitor, wrong_name)


@pytest.mark.parametrize('forbidden_name', [
'visitor',
'name_validator',
])
def test_name_in_forbidden_domain_names_option(
assert_errors,
assert_error_text,
parse_ast_tree,
naming_template,
options,
mode,
forbidden_name,
):
"""Ensures that names listed in `forbidden-domain-names` are forbidden."""
tree = parse_ast_tree(mode(naming_template.format(forbidden_name)))

visitor = WrongNameVisitor(
options(forbidden_domain_names=[forbidden_name]),
tree=tree,
)
visitor.run()

assert_errors(visitor, [WrongVariableNameViolation])
assert_error_text(visitor, forbidden_name)
19 changes: 19 additions & 0 deletions wemake_python_styleguide/logic/naming/blacklists.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-

from typing import FrozenSet

from wemake_python_styleguide.constants import VARIABLE_NAMES_BLACKLIST
from wemake_python_styleguide.types import ConfigurationOptions


def variable_names_blacklist_from(
options: ConfigurationOptions,
) -> FrozenSet[str]:
"""Creates variable names blacklist from options and constants."""
variable_names_blacklist = {
*VARIABLE_NAMES_BLACKLIST,
*options.forbidden_domain_names,
}
return frozenset(
variable_names_blacklist - set(options.allowed_domain_names),
)
18 changes: 18 additions & 0 deletions wemake_python_styleguide/options/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@
- ``max-noqa-comments`` - maximum number of `noqa` allowed in a module,
defaults to
:str:`wemake_python_styleguide.options.defaults.MAX_NOQA_COMMENTS`
- ``allowed-domain-names`` - list of allowed domain names, defaults to
:str:`wemake_python_styleguide.options.defaults.ALLOWED_DOMAIN_NAMES`
- ``forbidden-domain-names`` - list of forbidden domain names, defaults to
:str:`wemake_python_styleguide.options.defaults.FORBIDDEN_DOMAIN_NAMES`

.. rubric:: Complexity options

Expand Down Expand Up @@ -216,6 +220,20 @@ class Configuration(object):
type='string',
comma_separated_list=True,
),
_Option(
'--allowed-domain-names',
defaults.ALLOWED_DOMAIN_NAMES,
"Domain names that are removed from variable names' blacklist.",
type='string',
comma_separated_list=True,
),
_Option(
'--forbidden-domain-names',
defaults.FORBIDDEN_DOMAIN_NAMES,
"Domain names that extends variable names' blacklist.",
type='string',
comma_separated_list=True,
),

# Complexity:

Expand Down
6 changes: 6 additions & 0 deletions wemake_python_styleguide/options/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,9 @@

#: Maximum number of call chains.
MAX_CALL_LEVEL: Final = 3

#: Domain names that are removed from variable names' blacklist.
ALLOWED_DOMAIN_NAMES: Final = ()

#: Domain names that extends variable names' blacklist.
FORBIDDEN_DOMAIN_NAMES: Final = ()
29 changes: 29 additions & 0 deletions wemake_python_styleguide/options/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,29 @@ def factory(instance, attribute, field_value):
return factory


def validate_domain_names_options(
allowed_domain_names: List[str],
forbidden_domain_names: List[str],
) -> None:
"""Validator to check that allowed and forbidden names doesn't intersect.

Raises:
ValueError
"""
if not allowed_domain_names or not forbidden_domain_names:
return
intersecting_names = set(allowed_domain_names) & set(forbidden_domain_names)
if intersecting_names:
raise ValueError(
(
'Names passed to `allowed_domain_name` and ' +
'`forbidden_domain_name` cannot intersect. ' +
'Intersecting names: ' +
', '.join(intersecting_names)
),
)


@final
@attr.dataclass(slots=True)
class _ValidatedOptions(object):
Expand All @@ -42,6 +65,8 @@ class _ValidatedOptions(object):
validator=[_min_max(min=1, max=defaults.MAX_NOQA_COMMENTS)],
)
nested_classes_whitelist: List[str]
allowed_domain_names: List[str]
forbidden_domain_names: List[str]

# Complexity:
max_arguments: int = attr.ib(validator=[_min_max(min=1)])
Expand Down Expand Up @@ -71,6 +96,10 @@ class _ValidatedOptions(object):

def validate_options(options: ConfigurationOptions) -> _ValidatedOptions:
"""Validates all options from ``flake8``, uses a subset of them."""
validate_domain_names_options(
options.allowed_domain_names,
options.forbidden_domain_names,
)
fields_to_validate = [
field.name
for field in attr.fields(_ValidatedOptions)
Expand Down
2 changes: 2 additions & 0 deletions wemake_python_styleguide/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ class or structure.
max_name_length: int
max_noqa_comments: int
nested_classes_whitelist: List[str]
allowed_domain_names: List[str]
forbidden_domain_names: List[str]

# Complexity:
max_arguments: int
Expand Down
15 changes: 14 additions & 1 deletion wemake_python_styleguide/violations/naming.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ class WrongVariableNameViolation(ASTViolation):

See
:py:data:`~wemake_python_styleguide.constants.VARIABLE_NAMES_BLACKLIST`
for the full list of blacklisted variable names.
for the base list of blacklisted variable names.

Example::

Expand All @@ -298,6 +298,19 @@ class WrongVariableNameViolation(ASTViolation):
# Wrong:
item = None

Configuration:
This rule is configurable with ``--allowed-domain-names``.
Default:
:str:`wemake_python_styleguide.options.defaults.ALLOWED_DOMAIN_NAMES`

And with ``--forbidden-domain-names``.
Default:
:str:`wemake_python_styleguide.options.defaults.FORBIDDEN_DOMAIN_NAMES`

The options listed above are used to create new variable names'
blacklist starting from
:py:data:`~wemake_python_styleguide.constants.VARIABLE_NAMES_BLACKLIST`.

.. versionadded:: 0.1.0

"""
Expand Down
20 changes: 17 additions & 3 deletions wemake_python_styleguide/visitors/ast/naming.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,16 @@
import ast
import itertools
from collections import Counter
from typing import Callable, Iterable, List, Optional, Tuple, Union, cast
from typing import (
Callable,
FrozenSet,
Iterable,
List,
Optional,
Tuple,
Union,
cast,
)

from typing_extensions import final

Expand All @@ -12,11 +21,11 @@
from wemake_python_styleguide.constants import (
MODULE_METADATA_VARIABLES_BLACKLIST,
SPECIAL_ARGUMENT_NAMES_WHITELIST,
VARIABLE_NAMES_BLACKLIST,
)
from wemake_python_styleguide.logic import functions, nodes
from wemake_python_styleguide.logic.naming import (
access,
blacklists,
builtins,
logical,
name_nodes,
Expand Down Expand Up @@ -45,6 +54,8 @@
class _NameValidator(object):
"""Utility class to separate logic from the naming visitor."""

variable_names_blacklist: FrozenSet[str]

def __init__(
self,
error_callback: Callable[[base.BaseViolation], None],
Expand All @@ -53,6 +64,9 @@ def __init__(
"""Creates new instance of a name validator."""
self._error_callback = error_callback
self._options = options
self._variable_names_blacklist = (
blacklists.variable_names_blacklist_from(options)
)

def check_name(
self,
Expand All @@ -61,7 +75,7 @@ def check_name(
*,
is_first_argument: bool = False,
) -> None:
if logical.is_wrong_name(name, VARIABLE_NAMES_BLACKLIST):
if logical.is_wrong_name(name, self._variable_names_blacklist):
self._error_callback(
naming.WrongVariableNameViolation(node, text=name),
)
Expand Down