Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Semantic versioning in our case means:
### Bugfixes

- Fixes `WPS115` false-positive on `StrEnum`, `IntEnum`, `IntFlag` attributes, #3381

- Fixes `WPS432`, now it ignores magic numbers in `Literal`, #3397

## 1.1.0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,18 @@ def function_name(param1, param2: int = {0}):
set_definition = '{{"first", {0}, "other"}}'
tuple_definition = '({0}, )'

literal_type_hint = 'code: Literal[{0}] = {0}'
Comment thread
LordGvozd marked this conversation as resolved.
literal_type_hint_with_typing_module = 'code: typing.Literal[{0}] = {0}'
literal_type_hint_with_typing_exts_module = (
Comment thread
LordGvozd marked this conversation as resolved.
'code: typing_extensions.Literal[{0}] = {0}'
)
literal_type_hint_without_assign = 'code: Literal[{0}]'
literal_type_hint_typing_without_assign = 'code: typing.Literal[{0}]'
literal_type_hint_typing_ext_without_assign = (
'code: typing_extensions.Literal[{0}]'
)


# Wrong usages:

assignment_binop = 'final = {0} + 1'
Expand Down Expand Up @@ -68,6 +80,13 @@ def method(self):
some_dict[{0}]
"""

not_literal_type_hint = """
foo: Bar[{0}] = {0}
"""

literal_type_with_not_typing_module = 'foo: bar.Literal[{0}] = {0}'
literal_type_not_typing_module_no_assign = 'foo: bar.Literal[{0}]'


@pytest.mark.parametrize(
'code',
Expand All @@ -83,6 +102,12 @@ def method(self):
dict_definition_value,
set_definition,
tuple_definition,
literal_type_hint,
literal_type_hint_with_typing_module,
literal_type_hint_with_typing_exts_module,
literal_type_hint_without_assign,
literal_type_hint_typing_without_assign,
literal_type_hint_typing_ext_without_assign,
],
)
@pytest.mark.parametrize(
Expand Down Expand Up @@ -145,6 +170,8 @@ def test_magic_number(
inside_method,
list_index,
dict_key,
not_literal_type_hint,
literal_type_with_not_typing_module,
],
)
@pytest.mark.parametrize(
Expand Down Expand Up @@ -194,6 +221,9 @@ def test_magic_number_whitelist(
inside_method,
list_index,
dict_key,
not_literal_type_hint,
literal_type_with_not_typing_module,
literal_type_not_typing_module_no_assign,
],
)
@pytest.mark.parametrize(
Expand Down Expand Up @@ -244,6 +274,9 @@ def test_magic_number_warning(
inside_method,
list_index,
dict_key,
not_literal_type_hint,
literal_type_with_not_typing_module,
literal_type_not_typing_module_no_assign,
],
)
@pytest.mark.parametrize(
Expand Down
28 changes: 28 additions & 0 deletions wemake_python_styleguide/logic/complexity/annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"""

import ast
from collections.abc import Container
from typing import TypeAlias

_Annotation: TypeAlias = ast.expr | ast.Constant
Expand Down Expand Up @@ -44,3 +45,30 @@ def get_annotation_complexity(annotation_node: _Annotation) -> int:
default=1,
)
return 1


def check_is_node_in_specific_annotation(
node: ast.AST | None,
annotation_name: str,
annotation_modules: Container[str],
) -> bool:
"""
Check is node inside specific annotation.

Checks is ast node in annotation with name `annotation_name`
and is annotation module in `annotation_modules`.
"""
if isinstance(node, ast.Subscript):
if (
isinstance(node.value, ast.Attribute)
and isinstance(node.value.value, ast.Name)
and node.value.value.id in annotation_modules
and node.value.attr == annotation_name
):
return True
if (
isinstance(node.value, ast.Name)
and node.value.id in annotation_name
):
return True
return False
26 changes: 20 additions & 6 deletions wemake_python_styleguide/visitors/ast/builtins.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
FunctionNodes,
)
from wemake_python_styleguide.logic import nodes, source, walk
from wemake_python_styleguide.logic.complexity.annotations import (
check_is_node_in_specific_annotation,
)
from wemake_python_styleguide.logic.tree import (
attributes,
operators,
Expand Down Expand Up @@ -180,6 +183,12 @@ class WrongNumberVisitor(base.BaseNodeTokenVisitor):
)

_non_magic_modulo: ClassVar[int] = 10
_allowed_modules_to_literal_type_hint: ClassVar[frozenset[str]] = (
frozenset((
'typing',
'typing_extensions',
))
)

def visit_Num(self, node: ast.Constant) -> None:
"""Checks wrong constants inside the code."""
Expand All @@ -189,15 +198,20 @@ def visit_Num(self, node: ast.Constant) -> None:

def _check_is_magic(self, node: ast.Constant) -> None:
parent = operators.get_parent_ignoring_unary(node)
if isinstance(parent, self._allowed_parents):
return

if node.value in constants.MAGIC_NUMBERS_WHITELIST:
return
is_non_magic = (
isinstance(node.value, int) and node.value <= self._non_magic_modulo
)

if isinstance(node.value, int) and node.value <= self._non_magic_modulo:
if (
isinstance(parent, self._allowed_parents)
or node.value in constants.MAGIC_NUMBERS_WHITELIST
or is_non_magic
or check_is_node_in_specific_annotation(
parent, 'Literal', self._allowed_modules_to_literal_type_hint
)
):
return

try:
token = self._token_dict[node.lineno, node.col_offset]
except KeyError: # pragma: no cover
Expand Down