Skip to content

Commit

Permalink
Support naming validation for PEP695 (#2894)
Browse files Browse the repository at this point in the history
  • Loading branch information
sobolevn committed Mar 25, 2024
1 parent 884be48 commit bb8a278
Show file tree
Hide file tree
Showing 8 changed files with 106 additions and 23 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Semantic versioning in our case means:
https://docs.astral.sh/ruff/rules/useless-object-inheritance/
- **Breaking**: allow positional-only parameters,
since it is required by `mypy` when using `Concatenate`
- Adds support for naming rules for PEP695 type params
- Due to how `f`-string are parsed in `python3.12` several token-based
violations are not reported anymore for them:
`WrongMultilineStringViolation`, `ImplicitRawStringViolation`,
Expand Down
22 changes: 21 additions & 1 deletion tests/test_visitors/test_ast/test_naming/conftest.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import pytest

from wemake_python_styleguide.compat.constants import PY310
from wemake_python_styleguide.compat.constants import PY310, PY312
from wemake_python_styleguide.constants import UNUSED_PLACEHOLDER

# Imports:
Expand Down Expand Up @@ -189,6 +189,13 @@ def container():
...
"""

# Type parameters:

type_param_func = 'def some_value[{0}](): ...'
type_param_class = 'class SomeValue[{0}]: ...'
type_param_alias = 'type SomeValue[{0}] = ...'
type_alias_def = 'type {0} = ...'


# Fixtures:

Expand Down Expand Up @@ -254,6 +261,13 @@ def container():
match_inner,
match_star,
}
if PY312:
_ALL_FIXTURES |= {
type_param_func,
type_param_class,
type_param_alias,
type_alias_def,
}

_FOREIGN_NAMING_PATTERNS = frozenset((
foreign_attribute,
Expand All @@ -270,6 +284,12 @@ def container():
instance_attribute,
instance_typed_attribute,
)) | _FOREIGN_NAMING_PATTERNS
if PY312:
_ATTRIBUTES |= frozenset((
# Not really an attribute, but similar:
type_param_class,
))


_FORBIDDEN_UNUSED_TUPLE = frozenset((
unpacking_variables,
Expand Down
14 changes: 13 additions & 1 deletion wemake_python_styleguide/compat/functions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import ast
from typing import List, Union
from typing import List, Tuple, Union

from wemake_python_styleguide.compat.types import NodeWithTypeParams
from wemake_python_styleguide.types import AnyAssignWithWalrus


Expand All @@ -11,3 +12,14 @@ def get_assign_targets(
if isinstance(node, (ast.AnnAssign, ast.AugAssign, ast.NamedExpr)):
return [node.target]
return node.targets


def get_type_param_names( # pragma: py-lt-312
node: NodeWithTypeParams,
) -> List[Tuple[ast.AST, str]]:
"""Return list of type parameters' names."""
type_params = []
for type_param_node in getattr(node, 'type_params', []):
type_param_name = type_param_node.name
type_params.append((type_param_node, type_param_name))
return type_params
9 changes: 9 additions & 0 deletions wemake_python_styleguide/compat/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,12 @@ class TryStar(ast.stmt):
handlers: list[ast.ExceptHandler]
orelse: list[ast.stmt]
finalbody: list[ast.stmt]

if sys.version_info >= (3, 12): # pragma: py-lt-312
from ast import TypeAlias as TypeAlias
else: # pragma: py-gte-312
class TypeAlias(ast.stmt):
"""Used to define `TypeAlias` nodes in `python3.12+`."""

name: ast.Name
type_params: list[ast.stmt]
9 changes: 9 additions & 0 deletions wemake_python_styleguide/compat/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,18 @@
from typing_extensions import TypeAlias

from wemake_python_styleguide.compat.nodes import MatchAs, MatchStar, TryStar
from wemake_python_styleguide.compat.nodes import TypeAlias as TypeAliasNode

#: When used with `visit_Try` and visit_TryStar`.
AnyTry: TypeAlias = Union[ast.Try, TryStar]

#: Used when named matches are needed.
NamedMatch: TypeAlias = Union[MatchAs, MatchStar]

#: These nodes have `.type_params` on python3.12+:
NodeWithTypeParams: TypeAlias = Union[
ast.ClassDef,
ast.FunctionDef,
ast.AsyncFunctionDef,
TypeAliasNode,
]
8 changes: 4 additions & 4 deletions wemake_python_styleguide/logic/complexity/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,21 @@
from typing import DefaultDict, List

import attr
from typing_extensions import final
from typing_extensions import TypeAlias, final

from wemake_python_styleguide.types import (
AnyFunctionDef,
AnyFunctionDefAndLambda,
)

#: Function complexity counter.
FunctionCounter = DefaultDict[AnyFunctionDef, int]
FunctionCounter: TypeAlias = DefaultDict[AnyFunctionDef, int]

#: Function and lambda complexity counter.
FunctionCounterWithLambda = DefaultDict[AnyFunctionDefAndLambda, int]
FunctionCounterWithLambda: TypeAlias = DefaultDict[AnyFunctionDefAndLambda, int]

#: Function and their variables.
FunctionNames = DefaultDict[AnyFunctionDef, List[str]]
FunctionNames: TypeAlias = DefaultDict[AnyFunctionDef, List[str]]


def _default_factory() -> FunctionCounter:
Expand Down
18 changes: 17 additions & 1 deletion wemake_python_styleguide/logic/tree/attributes.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import ast
from typing import Iterable, Optional

from wemake_python_styleguide.types import AnyChainable
from wemake_python_styleguide.constants import SPECIAL_ARGUMENT_NAMES_WHITELIST
from wemake_python_styleguide.types import AnyChainable, AnyVariableDef


def _chained_item(iterator: ast.AST) -> Optional[ast.AST]:
Expand Down Expand Up @@ -33,3 +34,18 @@ def parts(node: AnyChainable) -> Iterable[ast.AST]:
if chained_item is None:
return
iterator = chained_item


def is_foreign_attribute(node: AnyVariableDef) -> bool:
"""Tells whether this node is a foreign attribute."""
if not isinstance(node, ast.Attribute):
return False

if not isinstance(node.value, ast.Name):
return True

# This condition finds attributes like `point.x`,
# but, ignores all other cases like `self.x`.
# So, we change the strictness of this rule,
# based on the attribute source.
return node.value.id not in SPECIAL_ARGUMENT_NAMES_WHITELIST
48 changes: 32 additions & 16 deletions wemake_python_styleguide/visitors/ast/naming/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@
import attr
from typing_extensions import final

from wemake_python_styleguide.compat.functions import get_assign_targets
from wemake_python_styleguide.compat.types import NamedMatch
from wemake_python_styleguide.compat.functions import (
get_assign_targets,
get_type_param_names,
)
from wemake_python_styleguide.compat.nodes import TypeAlias as TypeAliasNode
from wemake_python_styleguide.compat.types import NamedMatch, NodeWithTypeParams
from wemake_python_styleguide.constants import (
SPECIAL_ARGUMENT_NAMES_WHITELIST,
UNREADABLE_CHARACTER_COMBINATIONS,
Expand All @@ -18,7 +22,12 @@
logical,
name_nodes,
)
from wemake_python_styleguide.logic.tree import classes, functions, variables
from wemake_python_styleguide.logic.tree import (
attributes,
classes,
functions,
variables,
)
from wemake_python_styleguide.types import (
AnyAssign,
AnyFunctionDefAndLambda,
Expand Down Expand Up @@ -197,6 +206,16 @@ def check_function_signature(self, node: AnyFunctionDefAndLambda) -> None:
)


@final
class _TypeParamNameValidator(_RegularNameValidator):
def check_type_params( # pragma: py-lt-312
self,
node: NodeWithTypeParams,
) -> None:
for type_param_node, type_param_name in get_type_param_names(node):
self.check_name(type_param_node, type_param_name)


@final
class _ClassBasedNameValidator(_RegularNameValidator):
def check_attribute_names(self, node: ast.ClassDef) -> None:
Expand Down Expand Up @@ -253,6 +272,9 @@ def __init__(self, *args, **kwargs) -> None:
self._class_based_validator = _ClassBasedNameValidator(
self.add_violation, self.options,
)
self._type_params_validator = _TypeParamNameValidator(
self.add_violation, self.options,
)

def visit_any_import(self, node: AnyImport) -> None:
"""Used to check wrong import alias names."""
Expand All @@ -263,7 +285,7 @@ def visit_any_import(self, node: AnyImport) -> None:

def visit_variable(self, node: AnyVariableDef) -> None:
"""Used to check wrong names of assigned."""
validator = self._simple_validator if self._is_foreign_attribute(
validator = self._simple_validator if attributes.is_foreign_attribute(
node,
) else self._regular_validator

Expand All @@ -276,13 +298,15 @@ def visit_any_function(self, node: AnyFunctionDefAndLambda) -> None:
"""Used to find wrong function and method parameters."""
if not isinstance(node, ast.Lambda):
self._function_validator.check_name(node, node.name)
self._type_params_validator.check_type_params(node)
self._function_validator.check_function_signature(node)
self.generic_visit(node)

def visit_ClassDef(self, node: ast.ClassDef) -> None:
"""Used to find upper attribute declarations."""
self._class_based_validator.check_name(node, node.name)
self._class_based_validator.check_attribute_names(node)
self._type_params_validator.check_type_params(node)
self.generic_visit(node)

def visit_named_match(self, node: NamedMatch) -> None: # pragma: py-lt-310
Expand All @@ -299,15 +323,7 @@ def visit_named_match(self, node: NamedMatch) -> None: # pragma: py-lt-310
self._regular_validator.check_name(node, node.name)
self.generic_visit(node)

def _is_foreign_attribute(self, node: AnyVariableDef) -> bool:
if not isinstance(node, ast.Attribute):
return False

if not isinstance(node.value, ast.Name):
return True

# This condition finds attributes like `point.x`,
# but, ignores all other cases like `self.x`.
# So, we change the strictness of this rule,
# based on the attribute source.
return node.value.id not in SPECIAL_ARGUMENT_NAMES_WHITELIST
def visit_TypeAlias(self, node: TypeAliasNode) -> None: # pragma: py-lt-312
"""Visit PEP695 type aliases."""
self._type_params_validator.check_type_params(node)
self.generic_visit(node)

0 comments on commit bb8a278

Please sign in to comment.