Latest Threat Research:SANDWORM_MODE: Shai-Hulud-Style npm Worm Hijacks CI Workflows and Poisons AI Toolchains.Details
Socket
Book a DemoInstallSign in
Socket

wemake-python-styleguide

Package Overview
Dependencies
Maintainers
1
Versions
67
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

wemake-python-styleguide - npm Package Compare versions

Comparing version
1.0.0
to
1.1.0
wemake_python_styleguide/cli/__init__.py
+38
import argparse
from wemake_python_styleguide.cli.commands.explain.command import ExplainCommand
def _configure_arg_parser() -> argparse.ArgumentParser:
"""Configures CLI arguments and subcommands."""
parser = argparse.ArgumentParser(
prog='wps', description='WPS command line tool'
)
sub_parsers = parser.add_subparsers(
help='sub-parser for exact wps commands',
required=True,
)
parser_explain = sub_parsers.add_parser(
'explain',
help='Get violation description',
)
parser_explain.add_argument(
'violation_code',
help='Desired violation code',
)
parser_explain.set_defaults(func=ExplainCommand())
return parser
def parse_args() -> argparse.Namespace:
"""Parse CLI arguments."""
parser = _configure_arg_parser()
return parser.parse_args()
def main() -> int:
"""Main function."""
args = parse_args()
return int(args.func(args=args))
"""Contains files common for all wps commands."""
from abc import ABC, abstractmethod
from argparse import Namespace
from typing import Generic, TypeVar
_ArgsT = TypeVar('_ArgsT')
class AbstractCommand(ABC, Generic[_ArgsT]):
"""ABC for all commands."""
_args_type: type[_ArgsT]
def __call__(self, args: Namespace) -> int:
"""Parse arguments into the generic namespace."""
args_dict = vars(args) # noqa: WPS421
args_dict.pop('func') # argument classes do not expect that
cmd_args = self._args_type(**args_dict)
return self._run(cmd_args)
@abstractmethod
def _run(self, args: _ArgsT) -> int:
"""Run the command."""
raise NotImplementedError
"""Contains command implementation."""
from typing import final
from attrs import frozen
from wemake_python_styleguide.cli.commands.base import AbstractCommand
from wemake_python_styleguide.cli.commands.explain import (
message_formatter,
violation_loader,
)
from wemake_python_styleguide.cli.output import print_stderr, print_stdout
def _clean_violation_code(violation_str: str) -> int:
"""Get int violation code from str violation code."""
violation_str = violation_str.removeprefix('WPS')
try:
return int(violation_str)
except ValueError:
return -1
@final
@frozen
class ExplainCommandArgs:
"""Arguments for wps explain command."""
violation_code: str
@final
class ExplainCommand(AbstractCommand[ExplainCommandArgs]):
"""Explain command impl."""
_args_type = ExplainCommandArgs
def _run(self, args: ExplainCommandArgs) -> int:
"""Run command."""
code = _clean_violation_code(args.violation_code)
violation = violation_loader.get_violation(code)
if violation is None:
print_stderr(f'Violation "{args.violation_code}" not found')
return 1
message = message_formatter.format_violation(violation)
print_stdout(message)
return 0
"""Provides tools for formatting explanations."""
import textwrap
from wemake_python_styleguide.cli.commands.explain.violation_loader import (
ViolationInfo,
)
from wemake_python_styleguide.constants import SHORTLINK_TEMPLATE
def _remove_newlines_at_ends(text: str) -> str:
"""Remove leading and trailing newlines."""
return text.strip('\n\r')
def format_violation(violation: ViolationInfo) -> str:
"""Format violation information."""
cleaned_docstring = _remove_newlines_at_ends(
textwrap.dedent(violation.docstring)
)
violation_url = SHORTLINK_TEMPLATE.format(f'WPS{violation.code}')
return f'{cleaned_docstring}\n\nSee at website: {violation_url}'
import importlib
from collections.abc import Collection
from pathlib import Path
from types import ModuleType
from typing import Final
_VIOLATION_MODULE_BASE: Final = 'wemake_python_styleguide.violations'
def get_violation_submodules() -> Collection[ModuleType]:
"""Get all possible violation submodules."""
submodule_names = _get_all_possible_submodule_names(_VIOLATION_MODULE_BASE)
return [
importlib.import_module(submodule_name)
for submodule_name in submodule_names
]
def _get_all_possible_submodule_names(module_name: str) -> Collection[str]:
"""Get .py submodule names listed in given module."""
root_module = importlib.import_module(module_name)
root_paths = root_module.__path__
names = []
for root in root_paths:
names.extend([
f'{module_name}.{name}'
for name in _get_all_possible_names_in_root(root)
])
return names
def _get_all_possible_names_in_root(root: str) -> Collection[str]:
"""Get .py submodule names listed in given root path."""
return [
path.name.removesuffix('.py')
for path in Path(root).glob('*.py')
if '__' not in path.name # filter dunder files like __init__.py
]
"""Provides tools to extract violation info."""
import inspect
from collections.abc import Collection, Mapping
from types import ModuleType
from typing import final
from attrs import frozen
from wemake_python_styleguide.cli.commands.explain.module_loader import (
get_violation_submodules,
)
from wemake_python_styleguide.violations.base import BaseViolation
@final
@frozen
class ViolationInfo:
"""Contains violation info."""
identifier: str
code: int
docstring: str
section: str
def _is_a_violation(class_object) -> bool:
"""Check if class is a violation class."""
try:
return (
issubclass(class_object, BaseViolation)
and hasattr(class_object, 'code') # Only end-user classes have code
)
except TypeError: # pragma: no cover
# py 3.10 bug raises a type error
return False
def _get_violations_of_submodule(
module: ModuleType,
) -> Collection[type[BaseViolation]]:
"""Get all violation classes of defined module."""
return [
class_
for name, class_ in inspect.getmembers(module, inspect.isclass)
if _is_a_violation(class_)
]
def _create_violation_info(class_object, submodule_name: str) -> ViolationInfo:
"""Create violation info DTO from violation class and metadata."""
return ViolationInfo(
identifier=class_object.__name__,
code=class_object.code,
docstring=class_object.__doc__,
section=submodule_name,
)
def _get_all_violations() -> Mapping[int, ViolationInfo]:
"""Get all violations inside all defined WPS violation modules."""
all_violations = {}
for submodule in get_violation_submodules():
violations = _get_violations_of_submodule(submodule)
for violation in violations:
all_violations[violation.code] = _create_violation_info(
violation,
submodule.__name__,
)
return all_violations
def get_violation(code: int) -> ViolationInfo | None:
"""Get a violation by its integer code."""
violations = _get_all_violations()
if code not in violations:
return None
return violations[code]
"""Provides tool for outputting data."""
import sys
def print_stdout(*args: str) -> None:
"""Write usual text. Works as print."""
sys.stdout.write(' '.join(args))
sys.stdout.write('\n')
sys.stdout.flush()
def print_stderr(*args: str) -> None:
"""Write error text. Works as print."""
sys.stderr.write(' '.join(args))
sys.stderr.write('\n')
sys.stderr.flush()
from typing import Final
from wemake_python_styleguide.visitors.ast.classes import (
attributes,
classdef,
methods,
)
#: Used to store all classes related visitors to be later passed to checker:
PRESET: Final = (
classdef.WrongClassDefVisitor,
classdef.WrongClassBodyVisitor,
attributes.ClassAttributeVisitor,
attributes.WrongSlotsVisitor,
methods.WrongMethodVisitor,
methods.ClassMethodOrderVisitor,
methods.BuggySuperCallVisitor,
classdef.ConsecutiveDefaultTypeVarsVisitor,
)
import ast
from collections import defaultdict
from typing import ClassVar, final
from wemake_python_styleguide import types
from wemake_python_styleguide.compat.aliases import AssignNodes
from wemake_python_styleguide.compat.functions import get_assign_targets
from wemake_python_styleguide.logic import nodes, source, walk
from wemake_python_styleguide.logic.naming import name_nodes
from wemake_python_styleguide.logic.tree import (
attributes,
classes,
)
from wemake_python_styleguide.violations import oop
from wemake_python_styleguide.visitors import base, decorators
@final
class ClassAttributeVisitor(base.BaseNodeVisitor):
"""Finds incorrectattributes."""
def visit_ClassDef(self, node: ast.ClassDef) -> None:
"""Checks that assigned attributes are correct."""
self._check_attributes_shadowing(node)
self.generic_visit(node)
def visit_Lambda(self, node: ast.Lambda) -> None:
"""Finds `lambda` assigns in attributes."""
self._check_lambda_attribute(node)
self.generic_visit(node)
def _check_attributes_shadowing(self, node: ast.ClassDef) -> None:
if classes.is_dataclass(node):
# dataclasses by its nature allow class-level attributes
# shadowing from instance level.
return
class_attributes, instance_attributes = classes.get_attributes(
node,
include_annotated=False,
)
class_attribute_names = set(
name_nodes.flat_variable_names(class_attributes),
)
for instance_attr in instance_attributes:
if instance_attr.attr in class_attribute_names:
self.add_violation(
oop.ShadowedClassAttributeViolation(
instance_attr,
text=instance_attr.attr,
),
)
def _check_lambda_attribute(self, node: ast.Lambda) -> None:
assigned = walk.get_closest_parent(node, AssignNodes)
if not assigned or not isinstance(assigned, ast.Assign):
return # just used, not assigned
context = nodes.get_context(assigned)
if not isinstance(context, types.AnyFunctionDef) or not isinstance(
nodes.get_context(context),
ast.ClassDef,
):
return # it is not assigned in a method of a class
for attribute in assigned.targets:
if isinstance(
attribute, ast.Attribute
) and attributes.is_special_attr(attribute):
self.add_violation(oop.LambdaAttributeAssignedViolation(node))
@final
@decorators.alias(
'visit_any_assign',
(
'visit_Assign',
'visit_AnnAssign',
),
)
class WrongSlotsVisitor(base.BaseNodeVisitor):
"""Visits class attributes."""
_whitelisted_slots_nodes: ClassVar[types.AnyNodes] = (
ast.Tuple,
ast.Attribute,
ast.Subscript,
ast.Name,
ast.Call,
)
def visit_any_assign(self, node: types.AnyAssign) -> None:
"""Checks all assigns that have correct context."""
self._check_slots(node)
self.generic_visit(node)
def _contains_slots_assign(self, node: types.AnyAssign) -> bool:
for target in get_assign_targets(node):
if isinstance(target, ast.Name) and target.id == '__slots__':
return True
return False
def _count_slots_items(
self,
node: types.AnyAssign,
elements: ast.Tuple,
) -> None:
fields: defaultdict[str, list[ast.AST]] = defaultdict(list)
for tuple_item in elements.elts:
slot_name = self._slot_item_name(tuple_item)
if not slot_name:
self.add_violation(oop.WrongSlotsViolation(tuple_item))
return
fields[slot_name].append(tuple_item)
for slots in fields.values():
if not self._are_correct_slots(slots) or len(slots) > 1:
self.add_violation(oop.WrongSlotsViolation(node))
return
def _check_slots(self, node: types.AnyAssign) -> None:
if not isinstance(nodes.get_context(node), ast.ClassDef):
return
if not self._contains_slots_assign(node):
return
if not isinstance(node.value, self._whitelisted_slots_nodes):
self.add_violation(oop.WrongSlotsViolation(node))
return
if isinstance(node.value, ast.Tuple):
self._count_slots_items(node, node.value)
def _slot_item_name(self, node: ast.AST) -> str | None:
if isinstance(node, ast.Constant) and isinstance(node.value, str):
return node.value
if isinstance(node, ast.Starred):
return source.node_to_string(node)
return None
def _are_correct_slots(self, slots: list[ast.AST]) -> bool:
return all(
slot.value.isidentifier()
for slot in slots
if isinstance(slot, ast.Constant) and isinstance(slot.value, str)
)
import ast
from collections.abc import Sequence
from typing import ClassVar, final
from wemake_python_styleguide import types
from wemake_python_styleguide.compat.aliases import AssignNodes, FunctionNodes
from wemake_python_styleguide.compat.nodes import TypeVar, TypeVarTuple
from wemake_python_styleguide.logic.naming import enums
from wemake_python_styleguide.logic.tree import (
attributes,
classes,
getters_setters,
strings,
)
from wemake_python_styleguide.violations import best_practices as bp
from wemake_python_styleguide.violations import oop
from wemake_python_styleguide.violations.best_practices import (
SneakyTypeVarWithDefaultViolation,
)
from wemake_python_styleguide.visitors import base
@final
class WrongClassDefVisitor(base.BaseNodeVisitor):
"""
This class is responsible for restricting some ``class`` def anti-patterns.
Here we check for stylistic issues and design patterns.
"""
def visit_ClassDef(self, node: ast.ClassDef) -> None:
"""Checking class definitions."""
self._check_base_classes(node)
self._check_kwargs_unpacking(node)
self.generic_visit(node)
def _check_base_classes(self, node: ast.ClassDef) -> None:
for base_name in node.bases:
if not self._is_correct_base_class(base_name):
self.add_violation(oop.WrongBaseClassViolation(base_name))
continue
self._check_base_classes_rules(node, base_name)
def _is_correct_base_class(self, base_class: ast.AST) -> bool:
if isinstance(base_class, ast.Name):
return True
if isinstance(base_class, ast.Attribute):
return all(
isinstance(sub_node, ast.Name | ast.Attribute)
for sub_node in attributes.parts(base_class)
)
if isinstance(base_class, ast.Subscript):
parts = list(attributes.parts(base_class))
subscripts = list(
filter(
lambda part: isinstance(part, ast.Subscript),
parts,
),
)
correct_items = all(
isinstance(sub_node, ast.Name | ast.Attribute | ast.Subscript)
for sub_node in parts
)
return len(subscripts) == 1 and correct_items
return False
def _check_base_classes_rules(
self,
node: ast.ClassDef,
base_name: ast.expr,
) -> None:
id_attr = getattr(base_name, 'id', None)
if id_attr == 'BaseException':
self.add_violation(bp.BaseExceptionSubclassViolation(node))
elif classes.is_forbidden_super_class(
id_attr,
) and not enums.has_enum_base(node):
self.add_violation(
oop.BuiltinSubclassViolation(node, text=id_attr),
)
def _check_kwargs_unpacking(self, node: ast.ClassDef) -> None:
for keyword in node.keywords:
if keyword.arg is None:
self.add_violation(
bp.KwargsUnpackingInClassDefinitionViolation(node),
)
@final
class WrongClassBodyVisitor(base.BaseNodeVisitor):
"""
This class is responsible for restricting some ``class`` body anti-patterns.
Here we check for stylistic issues and design patterns.
"""
_allowed_body_nodes: ClassVar[types.AnyNodes] = (
*FunctionNodes,
ast.ClassDef, # we allow some nested classes
*AssignNodes, # fields and annotations
)
def visit_ClassDef(self, node: ast.ClassDef) -> None:
"""Checking class definitions."""
self._check_wrong_body_nodes(node)
self._check_getters_setters_methods(node)
self.generic_visit(node)
def _check_wrong_body_nodes(self, node: ast.ClassDef) -> None:
for sub_node in node.body:
if isinstance(sub_node, self._allowed_body_nodes):
continue
if strings.is_doc_string(sub_node):
continue
self.add_violation(oop.WrongClassBodyContentViolation(sub_node))
def _check_getters_setters_methods(self, node: ast.ClassDef) -> None:
getters_and_setters = set(
getters_setters.find_paired_getters_and_setters(node),
).union(
set( # To delete duplicated violations
getters_setters.find_attributed_getters_and_setters(node),
),
)
for method in getters_and_setters:
self.add_violation(
oop.UnpythonicGetterSetterViolation(
method,
text=method.name,
),
)
@final
class ConsecutiveDefaultTypeVarsVisitor(base.BaseNodeVisitor):
"""Responsible for finding TypeVarTuple after a TypeVar with default."""
def visit_ClassDef( # pragma: >=3.13 cover
self, node: ast.ClassDef
) -> None:
"""Check class definition for violation."""
if hasattr(node, 'type_params'): # pragma: >=3.13 cover
self._check_generics(node.type_params)
self.generic_visit(node)
def _check_generics( # pragma: >=3.13 cover
self, type_params: Sequence[ast.AST]
) -> None:
had_default = False
for type_param in type_params:
had_default = had_default or (
isinstance(type_param, TypeVar)
and type_param.default_value is not None
)
if had_default and isinstance(type_param, TypeVarTuple):
self.add_violation(
SneakyTypeVarWithDefaultViolation(type_param)
)
import ast
from typing import ClassVar, final
from wemake_python_styleguide import constants, types
from wemake_python_styleguide.compat.aliases import FunctionNodes
from wemake_python_styleguide.logic import nodes, walk
from wemake_python_styleguide.logic.arguments import function_args, super_args
from wemake_python_styleguide.logic.naming import access
from wemake_python_styleguide.logic.tree import (
functions,
strings,
)
from wemake_python_styleguide.violations import consistency, oop
from wemake_python_styleguide.visitors import base, decorators
@final
@decorators.alias(
'visit_any_function',
(
'visit_FunctionDef',
'visit_AsyncFunctionDef',
),
)
class WrongMethodVisitor(base.BaseNodeVisitor):
"""Visits functions, but treats them as methods."""
_special_async_iter: ClassVar[frozenset[str]] = frozenset(('__aiter__',))
def visit_any_function(self, node: types.AnyFunctionDef) -> None:
"""Checking class methods: async and regular."""
node_context = nodes.get_context(node)
if isinstance(node_context, ast.ClassDef):
self._check_bound_methods(node)
self._check_yield_magic_methods(node)
self._check_async_magic_methods(node)
self._check_useless_overwritten_methods(
node,
class_name=node_context.name,
)
self.generic_visit(node)
def _check_bound_methods(self, node: types.AnyFunctionDef) -> None:
if functions.is_staticmethod(node):
self.add_violation(oop.StaticMethodViolation(node))
elif not functions.get_all_arguments(node):
self.add_violation(
oop.MethodWithoutArgumentsViolation(node, text=node.name),
)
if node.name in constants.MAGIC_METHODS_BLACKLIST:
self.add_violation(
oop.BadMagicMethodViolation(node, text=node.name),
)
def _check_yield_magic_methods(self, node: types.AnyFunctionDef) -> None:
if isinstance(node, ast.AsyncFunctionDef):
return
if (
node.name in constants.YIELD_MAGIC_METHODS_BLACKLIST
and walk.is_contained(node, (ast.Yield, ast.YieldFrom))
):
self.add_violation(
oop.YieldMagicMethodViolation(node, text=node.name),
)
def _check_async_magic_methods(self, node: types.AnyFunctionDef) -> None:
if not isinstance(node, ast.AsyncFunctionDef):
return
if node.name in self._special_async_iter:
if not walk.is_contained(node, ast.Yield): # YieldFrom not async
self.add_violation(
oop.AsyncMagicMethodViolation(node, text=node.name),
)
elif node.name in constants.ASYNC_MAGIC_METHODS_BLACKLIST:
self.add_violation(
oop.AsyncMagicMethodViolation(node, text=node.name),
)
def _check_useless_overwritten_methods(
self,
node: types.AnyFunctionDef,
class_name: str,
) -> None:
if node.decorator_list:
# Any decorator can change logic and make this overwrite useful.
return
if node.args.defaults or list(filter(None, node.args.kw_defaults)):
# It means that function / method has defaults in args,
# we cannot be sure that these defaults are the same
# as in the call def, ignoring it.
return
call_stmt = self._get_call_stmt_of_useless_method(node)
if call_stmt is None or not isinstance(call_stmt.func, ast.Attribute):
return
attribute = call_stmt.func
defined_method_name = node.name
if defined_method_name != attribute.attr:
return
if not super_args.is_ordinary_super_call(
attribute.value, class_name
) or not function_args.is_call_matched_by_arguments(node, call_stmt):
return
self.add_violation(
oop.UselessOverwrittenMethodViolation(
node,
text=defined_method_name,
),
)
def _get_call_stmt_of_useless_method(
self,
node: types.AnyFunctionDef,
) -> ast.Call | None:
"""
Fetches ``super`` call statement from function definition.
Consider next body as possible candidate of useless method:
1. Optional[docstring]
2. single return statement with call
3. single statement with call, but without return
Related:
https://github.com/wemake-services/wemake-python-styleguide/issues/1168
"""
statements_number = len(node.body)
if statements_number > 2 or statements_number == 0:
return None
if statements_number == 2 and not strings.is_doc_string(node.body[0]):
return None
stmt = node.body[-1]
if isinstance(stmt, ast.Return):
call_stmt = stmt.value
return call_stmt if isinstance(call_stmt, ast.Call) else None
if isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Call):
return stmt.value
return None
@final
class ClassMethodOrderVisitor(base.BaseNodeVisitor):
"""Checks that all methods inside the class are ordered correctly."""
def visit_ClassDef(self, node: ast.ClassDef) -> None:
"""Ensures that class has correct methods order."""
self._check_method_order(node)
self.generic_visit(node)
def _check_method_order(self, node: ast.ClassDef) -> None:
method_nodes = [
subnode.name
for subnode in ast.walk(node)
if (
isinstance(subnode, FunctionNodes)
and nodes.get_context(subnode) is node
)
]
ideal = sorted(method_nodes, key=self._ideal_order, reverse=True)
for existing_order, ideal_order in zip(
method_nodes,
ideal,
strict=False,
):
if existing_order != ideal_order:
self.add_violation(consistency.WrongMethodOrderViolation(node))
return
def _ideal_order(self, first: str) -> int:
base_methods_order = {
'__init_subclass__': 7, # highest priority
'__new__': 6,
'__init__': 5,
'__call__': 4,
'__await__': 3,
}
public_and_magic_methods_priority = 2
if access.is_protected(first):
return 1
if access.is_private(first):
return 0 # lowest priority
return base_methods_order.get(first, public_and_magic_methods_priority)
@final
class BuggySuperCallVisitor(base.BaseNodeVisitor):
"""
Responsible for finding wrong form of `super()` call for certain contexts.
Call to `super()` without arguments will cause unexpected `TypeError` in a
number of specific contexts. Read more: https://bugs.python.org/issue46175
"""
_buggy_super_contexts: ClassVar[types.AnyNodes] = (
ast.GeneratorExp,
ast.SetComp,
ast.ListComp,
ast.DictComp,
)
def visit_Call(self, node: ast.Call) -> None:
"""Checks if this is a `super()` call in a specific context."""
self._check_buggy_super_context(node)
self.generic_visit(node)
def _check_buggy_super_context(self, node: ast.Call):
if not isinstance(node.func, ast.Name):
return
if node.func.id != 'super' or node.args:
return
# Check for being in a nested function
ctx = nodes.get_context(node)
if isinstance(ctx, FunctionNodes):
outer_ctx = nodes.get_context(ctx)
if isinstance(outer_ctx, FunctionNodes):
self.add_violation(oop.BuggySuperContextViolation(node))
return
if walk.get_closest_parent(node, self._buggy_super_contexts):
self.add_violation(oop.BuggySuperContextViolation(node))
+6
-6

@@ -1,6 +0,5 @@

Metadata-Version: 2.1
Metadata-Version: 2.3
Name: wemake-python-styleguide
Version: 1.0.0
Version: 1.1.0
Summary: The strictest and most opinionated python linter ever
Home-page: https://wemake-python-styleguide.rtfd.io
License: MIT

@@ -27,4 +26,5 @@ Keywords: flake8,flake8-plugin,flake8-formatter,linter,wemake.services,styleguide,code quality,pycqa

Requires-Dist: flake8 (>=7.1,<8.0)
Requires-Dist: pygments (>=2.4,<3.0)
Requires-Dist: pygments (>=2.5,<3.0)
Project-URL: Funding, https://opencollective.com/wemake-python-styleguide
Project-URL: Homepage, https://wemake-python-styleguide.rtfd.io
Project-URL: Repository, https://github.com/wemake-services/wemake-python-styleguide

@@ -37,3 +37,3 @@ Description-Content-Type: text/markdown

[![Supporters](https://img.shields.io/opencollective/all/wemake-python-styleguide.svg?color=gold&label=supporters)](https://opencollective.com/wemake-python-styleguide)
[![Build Status](https://github.com/wemake-services/wemake-python-styleguide/workflows/test/badge.svg?branch=master&event=push)](https://github.com/wemake-services/wemake-python-styleguide/actions?query=workflow%3Atest)
[![test](https://github.com/wemake-services/wemake-python-styleguide/actions/workflows/test.yml/badge.svg?branch=master&event=push)](https://github.com/wemake-services/wemake-python-styleguide/actions/workflows/test.yml)
[![codecov](https://codecov.io/gh/wemake-services/wemake-python-styleguide/branch/master/graph/badge.svg)](https://codecov.io/gh/wemake-services/wemake-python-styleguide)

@@ -126,3 +126,3 @@ [![Python Version](https://img.shields.io/pypi/pyversions/wemake-python-styleguide.svg)](https://pypi.org/project/wemake-python-styleguide/)

| Has a lot of strict rules? | ❌ | 🤔 | ❌ | ❌ | ✅ | ✅ |
| Has a lot of plugins? | ✅ | ❌ | ❌ | 🤔 | ❌ | ✅ |
| Has a lot of plugins? | ✅ | ❌ | ❌ | 🤔 | 🤔 | ✅ |

@@ -129,0 +129,0 @@ We have several primary objectives:

[build-system]
build-backend = "poetry.core.masonry.api"
requires = [ "poetry-core>=1.9" ]
requires = [ "poetry-core>=2.0" ]
[tool.poetry]
name = "wemake-python-styleguide"
version = "1.0.0"
version = "1.1.0"
description = "The strictest and most opinionated python linter ever"

@@ -57,3 +57,3 @@

attrs = "*"
pygments = "^2.4"
pygments = "^2.5"

@@ -78,4 +78,4 @@ [tool.poetry.group.dev.dependencies]

nbqa = "^1.2"
ruff = "^0.8"
black = "^24.10"
ruff = "^0.11"
black = "^25.1"

@@ -87,4 +87,5 @@ [tool.poetry.group.docs]

sphinx = "^8.1"
sphinx-autodoc-typehints = "^2.0"
sphinx-autodoc-typehints = "^3.0"
sphinxcontrib-mermaid = "^1.0"
furo = "^2024.8"
added-value = "^0.24"

@@ -108,2 +109,4 @@ tomli = "^2.0"

# Ruff config: https://docs.astral.sh/ruff/settings
preview = true
fix = true
target-version = "py310"

@@ -117,7 +120,8 @@ line-length = 80

preview = true
fix = true
format.quote-style = "single"
format.docstring-code-format = false
lint.select = [
[tool.ruff.format]
quote-style = "single"
docstring-code-format = false
[tool.ruff.lint]
select = [
"A", # flake8-builtins

@@ -162,3 +166,3 @@ "B", # flake8-bugbear

]
lint.ignore = [
ignore = [
"A005", # allow to shadow stdlib and builtin module names

@@ -182,3 +186,12 @@ "COM812", # trailing comma, conflicts with `ruff format`

]
lint.per-file-ignores."tests/*.py" = [
external = [ "WPS" ]
# Plugin configs:
flake8-import-conventions.banned-from = [ "ast" ]
flake8-quotes.inline-quotes = "single"
mccabe.max-complexity = 6
pydocstyle.convention = "google"
[tool.ruff.lint.per-file-ignores]
"tests/*.py" = [
"S101", # asserts

@@ -190,10 +203,5 @@ "S105", # hardcoded passwords

]
lint.per-file-ignores."wemake_python_styleguide/compat/nodes.py" = [ "ICN003", "PLC0414" ]
lint.per-file-ignores."wemake_python_styleguide/types.py" = [ "D102" ]
lint.per-file-ignores."wemake_python_styleguide/visitors/ast/*.py" = [ "N802" ]
lint.external = [ "WPS" ]
lint.flake8-import-conventions.banned-from = [ "ast" ]
lint.flake8-quotes.inline-quotes = "single"
lint.mccabe.max-complexity = 6
lint.pydocstyle.convention = "google"
"wemake_python_styleguide/compat/nodes.py" = [ "ICN003", "PLC0414" ]
"wemake_python_styleguide/types.py" = [ "D102" ]
"wemake_python_styleguide/visitors/ast/*.py" = [ "N802" ]

@@ -209,3 +217,4 @@ [tool.pytest.ini_options]

addopts = [
"--strict",
"--strict-config",
"--strict-markers",
"--doctest-modules",

@@ -233,6 +242,5 @@ # pytest-cov

omit = [
# Does not contain runtime logic:
"wemake_python_styleguide/types.py",
# All version specific tests:
"tests/**/*312.py",
"tests/**/*313.py"
]

@@ -279,1 +287,4 @@

warn_unused_ignores = false
[tool.poetry.scripts]
wps = "wemake_python_styleguide.cli.cli_app:main"

@@ -5,3 +5,3 @@ # wemake-python-styleguide

[![Supporters](https://img.shields.io/opencollective/all/wemake-python-styleguide.svg?color=gold&label=supporters)](https://opencollective.com/wemake-python-styleguide)
[![Build Status](https://github.com/wemake-services/wemake-python-styleguide/workflows/test/badge.svg?branch=master&event=push)](https://github.com/wemake-services/wemake-python-styleguide/actions?query=workflow%3Atest)
[![test](https://github.com/wemake-services/wemake-python-styleguide/actions/workflows/test.yml/badge.svg?branch=master&event=push)](https://github.com/wemake-services/wemake-python-styleguide/actions/workflows/test.yml)
[![codecov](https://codecov.io/gh/wemake-services/wemake-python-styleguide/branch/master/graph/badge.svg)](https://codecov.io/gh/wemake-services/wemake-python-styleguide)

@@ -94,3 +94,3 @@ [![Python Version](https://img.shields.io/pypi/pyversions/wemake-python-styleguide.svg)](https://pypi.org/project/wemake-python-styleguide/)

| Has a lot of strict rules? | ❌ | 🤔 | ❌ | ❌ | ✅ | ✅ |
| Has a lot of plugins? | ✅ | ❌ | ❌ | 🤔 | ❌ | ✅ |
| Has a lot of plugins? | ✅ | ❌ | ❌ | 🤔 | 🤔 | ✅ |

@@ -97,0 +97,0 @@ We have several primary objectives:

@@ -27,4 +27,2 @@ """

.. _checker:
Checker API

@@ -31,0 +29,0 @@ -----------

@@ -9,1 +9,4 @@ import sys

PY312: Final = sys.version_info >= (3, 12)
# This indicates that we are running on python3.13+
PY313: Final = sys.version_info >= (3, 13)

@@ -34,1 +34,19 @@ """

value: ast.expr # noqa: WPS110
if sys.version_info >= (3, 13): # pragma: >=3.13 cover
from ast import TypeVar as TypeVar
from ast import TypeVarTuple as TypeVarTuple
else: # pragma: <3.13 cover
class TypeVar(ast.AST):
"""Used to define `TypeVar` nodes from `python3.12+`."""
name: str
bound: ast.expr | None # noqa: WPS110
default_value: ast.AST | None
class TypeVarTuple(ast.AST):
"""Used to define `TypeVarTuple` nodes from `python3.12+`."""
name: str

@@ -46,2 +46,5 @@ """

#: This url points to the specific violation page.
SHORTLINK_TEMPLATE: Final = 'https://pyflak.es/{0}'
#: List of functions we forbid to use.

@@ -48,0 +51,0 @@ FUNCTIONS_BLACKLIST: Final = frozenset(

@@ -39,2 +39,3 @@ """

from wemake_python_styleguide.constants import SHORTLINK_TEMPLATE
from wemake_python_styleguide.version import pkg_version

@@ -47,5 +48,2 @@

#: This url points to the specific violation page.
_SHORTLINK_TEMPLATE: Final = 'https://pyflak.es/{0}'
#: Option to disable any code highlight and text output format.

@@ -167,3 +165,3 @@ #: See https://no-color.org

spacing=' ' * 9,
link=_SHORTLINK_TEMPLATE.format(error.code),
link=SHORTLINK_TEMPLATE.format(error.code),
)

@@ -170,0 +168,0 @@

import ast
from collections.abc import Collection
from typing import Final

@@ -15,6 +16,33 @@

_ENUM_LIKE_NAMES: Final = (
*_ENUM_NAMES,
'Choices',
'models.Choices',
'IntegerChoices',
'models.IntegerChoices',
'TextChoices',
'models.TextChoices',
)
def _has_one_of_base_classes(
defn: ast.ClassDef, base_names: Collection[str]
) -> bool:
"""Tells whether some class has one of provided names as its base."""
string_bases = {node_to_string(base) for base in defn.bases}
return any(enum_base in string_bases for enum_base in base_names)
def has_enum_base(defn: ast.ClassDef) -> bool:
"""Tells whether some class has `Enum` or similar class as its base."""
string_bases = {node_to_string(base) for base in defn.bases}
return any(enum_base in string_bases for enum_base in _ENUM_NAMES)
return _has_one_of_base_classes(defn, _ENUM_NAMES)
def has_enum_like_base(defn: ast.ClassDef) -> bool:
"""
Tells if some class has `Enum` or semantically similar class as its base.
Unlike ``has_enum_base`` it also includes support for Django Choices.
https://docs.djangoproject.com/en/5.1/ref/models/fields/#choices
"""
return _has_one_of_base_classes(defn, _ENUM_LIKE_NAMES)

@@ -26,3 +26,5 @@ import ast

_STATICMETHOD_NAMES: Final = frozenset(('staticmethod',))
def given_function_called(

@@ -107,1 +109,10 @@ node: ast.Call,

return False
def is_staticmethod(node: AnyFunctionDef) -> bool:
"""Check that function decorated with @staticmethod."""
for decorator in node.decorator_list:
decorator_name = getattr(decorator, 'id', None)
if decorator_name in _STATICMETHOD_NAMES:
return True
return False
from typing import Final
from wemake_python_styleguide.presets.topics import complexity, naming
from wemake_python_styleguide.presets.topics import classes, complexity, naming
from wemake_python_styleguide.visitors.ast import ( # noqa: WPS235
blocks,
builtins,
classes,
compares,

@@ -45,2 +44,3 @@ conditions,

loops.WrongLoopDefinitionVisitor,
loops.WrongStatementInLoopVisitor,
functions.WrongFunctionCallVisitor,

@@ -74,9 +74,2 @@ functions.FunctionDefinitionVisitor,

iterables.IterableUnpackingVisitor,
classes.WrongClassDefVisitor,
classes.WrongClassBodyVisitor,
classes.WrongMethodVisitor,
classes.WrongSlotsVisitor,
classes.ClassAttributeVisitor,
classes.ClassMethodOrderVisitor,
classes.BuggySuperCallVisitor,
blocks.AfterBlockVariablesVisitor,

@@ -96,2 +89,3 @@ subscripts.SubscriptVisitor,

*naming.PRESET,
*classes.PRESET,
)

@@ -82,9 +82,2 @@ """

Each subclass must define ``error_template`` and ``code`` fields.
Attributes:
error_template: message that will be shown to user after formatting.
code: unique violation number. Used to identify the violation.
disabled_since: indicates that this violation is disabled.
postfix_template: indicates message that we show at the very end.
"""

@@ -91,0 +84,0 @@

@@ -722,2 +722,5 @@ """

3. type annotations, since they do not increase the complexity
4. nodes produced by ``{}`` (but not their contents!) in f-strings
as they raise the complexity score disproportionally to the
actual complexity raise

@@ -885,2 +888,4 @@ Reasoning:

The violation points to the first occurrence of the overused string literal.
Reasoning:

@@ -1151,3 +1156,3 @@ When some string is used more than several time in your code,

"""
Forbid ``from ... import ...`` with too many imported names.
Forbid ``from mod import a, b, c, d`` with too many imported names.

@@ -1154,0 +1159,0 @@ Reasoning:

@@ -615,2 +615,5 @@ """

.. versionadded:: 0.3.0
.. versionchanged:: 1.0.1
No longer produced, kept here for historic reasons.
This is covered with ``ruff`` and ``pylint`` linters. See ``PLR0124``.

@@ -621,2 +624,3 @@ """

code = 312
disabled_since = '1.0.1'

@@ -623,0 +627,0 @@

@@ -491,2 +491,5 @@ """

Attributes in Enum and enum-like classes (Django Choices)
are ignored, as they should be written in UPPER_SNAKE_CASE
Reasoning:

@@ -507,2 +510,7 @@ Constants with upper-case names belong on a module level.

# Correct:
class Color(enum.Enum):
WHITE = 0
LIGHT_GRAY = 1
# Wrong:

@@ -509,0 +517,0 @@ class A:

@@ -253,3 +253,3 @@ """

Methods without arguments are allowed to be defined,
but almost impossible to use.
but almost impossible to use, if they are not @staticmethods.
Furthermore, they don't have an access to ``self``,

@@ -275,2 +275,4 @@ so cannot access the inner state of the object.

.. versionchanged:: 0.11.0
.. versionchanged:: 1.1.0
Allows usage of ``@staticmethod`` with no arguments.

@@ -277,0 +279,0 @@ """

@@ -1424,3 +1424,3 @@ """

"""
Forbid extra syntax around ``match`` like ``[]`` or ``{ ... }``.
Forbid extra syntax around ``match`` like list, set, or dict.

@@ -1427,0 +1427,0 @@ Reasoning:

@@ -167,3 +167,3 @@ import ast

@final
class WrongNumberVisitor(base.BaseNodeVisitor):
class WrongNumberVisitor(base.BaseNodeTokenVisitor):
"""Checks wrong numbers used in the code."""

@@ -202,4 +202,14 @@

try:
token = self._token_dict[node.lineno, node.col_offset]
except KeyError: # pragma: no cover
# For some reason, the token was not found.
# We are not sure that this will actually happen,
# and cannot really replicate this. Yet. But, better be safe.
real_value = str(node.value)
else:
real_value = token.string
self.add_violation(
best_practices.MagicNumberViolation(node, text=str(node.value)),
best_practices.MagicNumberViolation(node, text=real_value),
)

@@ -206,0 +216,0 @@

@@ -5,3 +5,2 @@ import ast

from wemake_python_styleguide.logic import nodes, walk
from wemake_python_styleguide.logic.naming.name_nodes import is_same_variable
from wemake_python_styleguide.logic.tree import (

@@ -23,3 +22,2 @@ compares,

ReversedComplexCompareViolation,
UselessCompareViolation,
)

@@ -43,3 +41,2 @@ from wemake_python_styleguide.violations.refactoring import (

self._check_literal_compare(node)
self._check_useless_compare(node)
self._check_heterogeneous_operators(node)

@@ -58,10 +55,2 @@ self._check_reversed_complex_compare(node)

def _check_useless_compare(self, node: ast.Compare) -> None:
last_variable = get_assigned_expr(node.left)
for next_variable in map(get_assigned_expr, node.comparators):
if is_same_variable(last_variable, next_variable):
self.add_violation(UselessCompareViolation(node))
break
last_variable = next_variable
def _check_heterogeneous_operators(self, node: ast.Compare) -> None:

@@ -68,0 +57,0 @@ if len(node.ops) == 1:

@@ -31,7 +31,8 @@ """

Also calculates the median nodes/line score.
Then compares these numbers to the given tressholds.
Then compares these numbers to the given thresholds.
Some nodes are ignored because there's no sense in analyzing them.
Some nodes like type annotations are not affecting line complexity,
so we do not count them.
so we do not count them. FormattedValue and JoinedStr nodes are not
counted, because they have no visible impact on source code.
"""

@@ -43,2 +44,4 @@

ast.expr_context,
ast.FormattedValue,
ast.JoinedStr,
)

@@ -45,0 +48,0 @@

@@ -70,2 +70,7 @@ import ast

self._string_constants_first_node: defaultdict[
AnyTextPrimitive,
ast.Constant,
] = defaultdict(lambda: ast.Constant(value=None))
def visit_any_string(self, node: ast.Constant) -> None:

@@ -85,2 +90,5 @@ """Restricts to over-use string constants."""

if node.value not in self._string_constants_first_node:
self._string_constants_first_node[node.value] = node
self._string_constants[node.value] += 1

@@ -95,2 +103,3 @@

baseline=self.options.max_string_usages,
node=self._string_constants_first_node[string],
),

@@ -97,0 +106,0 @@ )

@@ -6,2 +6,4 @@ import ast

from attrs import frozen
from wemake_python_styleguide.constants import FUTURE_IMPORTS_WHITELIST

@@ -30,12 +32,8 @@ from wemake_python_styleguide.logic import nodes

@frozen
class _BaseImportValidator:
"""Base utility class to separate logic from the visitor."""
def __init__(
self,
error_callback: ErrorCallback,
options: ValidatedOptions,
) -> None:
self._error_callback = error_callback
self._options = options
_error_callback: ErrorCallback
_options: ValidatedOptions

@@ -42,0 +40,0 @@

@@ -19,2 +19,3 @@ import ast

from wemake_python_styleguide.violations.best_practices import (
AwaitInLoopViolation,
InfiniteWhileLoopViolation,

@@ -239,1 +240,29 @@ LambdaInsideLoopViolation,

self.add_violation(ImplicitSumViolation(node))
@final
class WrongStatementInLoopVisitor(base.BaseNodeVisitor):
"""Responsible for statements inside loops."""
_forbidden_await_loops: ClassVar[AnyNodes] = (
ast.For,
ast.DictComp,
ast.GeneratorExp,
ast.ListComp,
ast.SetComp,
)
def visit_Await(self, node: ast.Await):
"""Visits ``await`` in loops and check it appropriation to use."""
self._check_await_inside_loop(node)
self.generic_visit(node)
def _check_await_inside_loop(self, node: ast.Await) -> None:
node_parent = walk.get_closest_parent(node, self._forbidden_await_loops)
if isinstance(node_parent, AnyComprehension) and all(
comprehension.is_async for comprehension in node_parent.generators
):
# async comprehensions are allowed to use `await`
return
if node_parent is not None:
self.add_violation(AwaitInLoopViolation(node))

@@ -22,2 +22,3 @@ import ast

builtins,
enums,
logical,

@@ -116,3 +117,3 @@ name_nodes,

if not logical.is_wrong_name(name, SPECIAL_ARGUMENT_NAMES_WHITELIST):
if name not in SPECIAL_ARGUMENT_NAMES_WHITELIST:
return

@@ -232,2 +233,3 @@

)
is_enum_like = enums.has_enum_like_base(node)

@@ -237,6 +239,16 @@ for assign in class_attributes:

for attr_name in name_nodes.get_variables_from_node(target):
self._ensure_case(assign, attr_name)
self._ensure_case(
assign,
attr_name,
is_enum_like=is_enum_like,
)
def _ensure_case(self, node: AnyAssign, name: str) -> None:
if logical.is_upper_case_name(name):
def _ensure_case(
self,
node: AnyAssign,
name: str,
*,
is_enum_like: bool,
) -> None:
if not is_enum_like and logical.is_upper_case_name(name):
self._error_callback(

@@ -243,0 +255,0 @@ naming.UpperCaseAttributeViolation(node, text=name),

@@ -201,6 +201,2 @@ """

Has ``visit_filename()`` method that should be defined in subclasses.
Attributes:
stem: the last part of the filename. Does not contain extension.
"""

@@ -290,1 +286,47 @@

self._post_visit()
class BaseNodeTokenVisitor(ast.NodeVisitor, BaseVisitor):
"""Allows storing violations during node tree traversal with real values."""
def __init__(
self,
options: ValidatedOptions,
tree: ast.AST,
file_tokens: Sequence[tokenize.TokenInfo],
**kwargs,
) -> None:
"""Creates new ``ast`` based instance with tokens."""
super().__init__(options, **kwargs)
self.tree = tree
self.file_tokens = file_tokens
self._token_index = -1
self._token_dict: dict[tuple[int, int], tokenize.TokenInfo] = {}
@final
@classmethod
def from_checker(
cls: type['BaseNodeTokenVisitor'], checker
) -> 'BaseNodeTokenVisitor':
"""Constructs visitor instance from the checker."""
return cls(
options=checker.options,
filename=checker.filename,
file_tokens=checker.file_tokens,
tree=checker.tree,
)
def visit(self, tree: ast.AST) -> None:
"""This method does the same as :meth:`BaseNodeVisitor.visit`."""
return route_visit(self, tree)
@final
def run(self) -> None:
"""Recursively visits all ``ast`` nodes and create ``token_dict``."""
self._create_token_dict()
self.visit(self.tree)
self._post_visit()
def _create_token_dict(self) -> None:
"""Create a token dict."""
self._token_dict = {token.start: token for token in self.file_tokens}
import ast
from collections import defaultdict
from typing import ClassVar, final
from wemake_python_styleguide import constants, types
from wemake_python_styleguide.compat.aliases import AssignNodes, FunctionNodes
from wemake_python_styleguide.compat.functions import get_assign_targets
from wemake_python_styleguide.logic import nodes, source, walk
from wemake_python_styleguide.logic.arguments import function_args, super_args
from wemake_python_styleguide.logic.naming import access, enums, name_nodes
from wemake_python_styleguide.logic.tree import (
attributes,
classes,
functions,
getters_setters,
strings,
)
from wemake_python_styleguide.violations import best_practices as bp
from wemake_python_styleguide.violations import consistency, oop
from wemake_python_styleguide.visitors import base, decorators
@final
class WrongClassDefVisitor(base.BaseNodeVisitor):
"""
This class is responsible for restricting some ``class`` def anti-patterns.
Here we check for stylistic issues and design patterns.
"""
def visit_ClassDef(self, node: ast.ClassDef) -> None:
"""Checking class definitions."""
self._check_base_classes(node)
self._check_kwargs_unpacking(node)
self.generic_visit(node)
def _check_base_classes(self, node: ast.ClassDef) -> None:
for base_name in node.bases:
if not self._is_correct_base_class(base_name):
self.add_violation(oop.WrongBaseClassViolation(base_name))
continue
self._check_base_classes_rules(node, base_name)
def _is_correct_base_class(self, base_class: ast.AST) -> bool:
if isinstance(base_class, ast.Name):
return True
if isinstance(base_class, ast.Attribute):
return all(
isinstance(sub_node, ast.Name | ast.Attribute)
for sub_node in attributes.parts(base_class)
)
if isinstance(base_class, ast.Subscript):
parts = list(attributes.parts(base_class))
subscripts = list(
filter(
lambda part: isinstance(part, ast.Subscript),
parts,
),
)
correct_items = all(
isinstance(sub_node, ast.Name | ast.Attribute | ast.Subscript)
for sub_node in parts
)
return len(subscripts) == 1 and correct_items
return False
def _check_base_classes_rules(
self,
node: ast.ClassDef,
base_name: ast.expr,
) -> None:
id_attr = getattr(base_name, 'id', None)
if id_attr == 'BaseException':
self.add_violation(bp.BaseExceptionSubclassViolation(node))
elif classes.is_forbidden_super_class(
id_attr,
) and not enums.has_enum_base(node):
self.add_violation(
oop.BuiltinSubclassViolation(node, text=id_attr),
)
def _check_kwargs_unpacking(self, node: ast.ClassDef) -> None:
for keyword in node.keywords:
if keyword.arg is None:
self.add_violation(
bp.KwargsUnpackingInClassDefinitionViolation(node),
)
@final
class WrongClassBodyVisitor(base.BaseNodeVisitor):
"""
This class is responsible for restricting some ``class`` body anti-patterns.
Here we check for stylistic issues and design patterns.
"""
_allowed_body_nodes: ClassVar[types.AnyNodes] = (
*FunctionNodes,
ast.ClassDef, # we allow some nested classes
*AssignNodes, # fields and annotations
)
def visit_ClassDef(self, node: ast.ClassDef) -> None:
"""Checking class definitions."""
self._check_wrong_body_nodes(node)
self._check_getters_setters_methods(node)
self.generic_visit(node)
def _check_wrong_body_nodes(self, node: ast.ClassDef) -> None:
for sub_node in node.body:
if isinstance(sub_node, self._allowed_body_nodes):
continue
if strings.is_doc_string(sub_node):
continue
self.add_violation(oop.WrongClassBodyContentViolation(sub_node))
def _check_getters_setters_methods(self, node: ast.ClassDef) -> None:
getters_and_setters = set(
getters_setters.find_paired_getters_and_setters(node),
).union(
set( # To delete duplicated violations
getters_setters.find_attributed_getters_and_setters(node),
),
)
for method in getters_and_setters:
self.add_violation(
oop.UnpythonicGetterSetterViolation(
method,
text=method.name,
),
)
@final
@decorators.alias(
'visit_any_function',
(
'visit_FunctionDef',
'visit_AsyncFunctionDef',
),
)
class WrongMethodVisitor(base.BaseNodeVisitor):
"""Visits functions, but treats them as methods."""
_staticmethod_names: ClassVar[frozenset[str]] = frozenset(('staticmethod',))
_special_async_iter: ClassVar[frozenset[str]] = frozenset(('__aiter__',))
def visit_any_function(self, node: types.AnyFunctionDef) -> None:
"""Checking class methods: async and regular."""
node_context = nodes.get_context(node)
if isinstance(node_context, ast.ClassDef):
self._check_decorators(node)
self._check_bound_methods(node)
self._check_yield_magic_methods(node)
self._check_async_magic_methods(node)
self._check_useless_overwritten_methods(
node,
class_name=node_context.name,
)
self.generic_visit(node)
def _check_decorators(self, node: types.AnyFunctionDef) -> None:
for decorator in node.decorator_list:
decorator_name = getattr(decorator, 'id', None)
if decorator_name in self._staticmethod_names:
self.add_violation(oop.StaticMethodViolation(node))
def _check_bound_methods(self, node: types.AnyFunctionDef) -> None:
if not functions.get_all_arguments(node):
self.add_violation(
oop.MethodWithoutArgumentsViolation(node, text=node.name),
)
if node.name in constants.MAGIC_METHODS_BLACKLIST:
self.add_violation(
oop.BadMagicMethodViolation(node, text=node.name),
)
def _check_yield_magic_methods(self, node: types.AnyFunctionDef) -> None:
if isinstance(node, ast.AsyncFunctionDef):
return
if (
node.name in constants.YIELD_MAGIC_METHODS_BLACKLIST
and walk.is_contained(node, (ast.Yield, ast.YieldFrom))
):
self.add_violation(
oop.YieldMagicMethodViolation(node, text=node.name),
)
def _check_async_magic_methods(self, node: types.AnyFunctionDef) -> None:
if not isinstance(node, ast.AsyncFunctionDef):
return
if node.name in self._special_async_iter:
if not walk.is_contained(node, ast.Yield): # YieldFrom not async
self.add_violation(
oop.AsyncMagicMethodViolation(node, text=node.name),
)
elif node.name in constants.ASYNC_MAGIC_METHODS_BLACKLIST:
self.add_violation(
oop.AsyncMagicMethodViolation(node, text=node.name),
)
def _check_useless_overwritten_methods(
self,
node: types.AnyFunctionDef,
class_name: str,
) -> None:
if node.decorator_list:
# Any decorator can change logic and make this overwrite useful.
return
if node.args.defaults or list(filter(None, node.args.kw_defaults)):
# It means that function / method has defaults in args,
# we cannot be sure that these defaults are the same
# as in the call def, ignoring it.
return
call_stmt = self._get_call_stmt_of_useless_method(node)
if call_stmt is None or not isinstance(call_stmt.func, ast.Attribute):
return
attribute = call_stmt.func
defined_method_name = node.name
if defined_method_name != attribute.attr:
return
if not super_args.is_ordinary_super_call(
attribute.value, class_name
) or not function_args.is_call_matched_by_arguments(node, call_stmt):
return
self.add_violation(
oop.UselessOverwrittenMethodViolation(
node,
text=defined_method_name,
),
)
def _get_call_stmt_of_useless_method(
self,
node: types.AnyFunctionDef,
) -> ast.Call | None:
"""
Fetches ``super`` call statement from function definition.
Consider next body as possible candidate of useless method:
1. Optional[docstring]
2. single return statement with call
3. single statement with call, but without return
Related:
https://github.com/wemake-services/wemake-python-styleguide/issues/1168
"""
statements_number = len(node.body)
if statements_number > 2 or statements_number == 0:
return None
if statements_number == 2 and not strings.is_doc_string(node.body[0]):
return None
stmt = node.body[-1]
if isinstance(stmt, ast.Return):
call_stmt = stmt.value
return call_stmt if isinstance(call_stmt, ast.Call) else None
if isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Call):
return stmt.value
return None
@final
@decorators.alias(
'visit_any_assign',
(
'visit_Assign',
'visit_AnnAssign',
),
)
class WrongSlotsVisitor(base.BaseNodeVisitor):
"""Visits class attributes."""
_whitelisted_slots_nodes: ClassVar[types.AnyNodes] = (
ast.Tuple,
ast.Attribute,
ast.Subscript,
ast.Name,
ast.Call,
)
def visit_any_assign(self, node: types.AnyAssign) -> None:
"""Checks all assigns that have correct context."""
self._check_slots(node)
self.generic_visit(node)
def _contains_slots_assign(self, node: types.AnyAssign) -> bool:
for target in get_assign_targets(node):
if isinstance(target, ast.Name) and target.id == '__slots__':
return True
return False
def _count_slots_items(
self,
node: types.AnyAssign,
elements: ast.Tuple,
) -> None:
fields: defaultdict[str, list[ast.AST]] = defaultdict(list)
for tuple_item in elements.elts:
slot_name = self._slot_item_name(tuple_item)
if not slot_name:
self.add_violation(oop.WrongSlotsViolation(tuple_item))
return
fields[slot_name].append(tuple_item)
for slots in fields.values():
if not self._are_correct_slots(slots) or len(slots) > 1:
self.add_violation(oop.WrongSlotsViolation(node))
return
def _check_slots(self, node: types.AnyAssign) -> None:
if not isinstance(nodes.get_context(node), ast.ClassDef):
return
if not self._contains_slots_assign(node):
return
if not isinstance(node.value, self._whitelisted_slots_nodes):
self.add_violation(oop.WrongSlotsViolation(node))
return
if isinstance(node.value, ast.Tuple):
self._count_slots_items(node, node.value)
def _slot_item_name(self, node: ast.AST) -> str | None:
if isinstance(node, ast.Constant) and isinstance(node.value, str):
return node.value
if isinstance(node, ast.Starred):
return source.node_to_string(node)
return None
def _are_correct_slots(self, slots: list[ast.AST]) -> bool:
return all(
slot.value.isidentifier()
for slot in slots
if isinstance(slot, ast.Constant) and isinstance(slot.value, str)
)
@final
class ClassAttributeVisitor(base.BaseNodeVisitor):
"""Finds incorrectattributes."""
def visit_ClassDef(self, node: ast.ClassDef) -> None:
"""Checks that assigned attributes are correct."""
self._check_attributes_shadowing(node)
self.generic_visit(node)
def visit_Lambda(self, node: ast.Lambda) -> None:
"""Finds `lambda` assigns in attributes."""
self._check_lambda_attribute(node)
self.generic_visit(node)
def _check_attributes_shadowing(self, node: ast.ClassDef) -> None:
if classes.is_dataclass(node):
# dataclasses by its nature allow class-level attributes
# shadowing from instance level.
return
class_attributes, instance_attributes = classes.get_attributes(
node,
include_annotated=False,
)
class_attribute_names = set(
name_nodes.flat_variable_names(class_attributes),
)
for instance_attr in instance_attributes:
if instance_attr.attr in class_attribute_names:
self.add_violation(
oop.ShadowedClassAttributeViolation(
instance_attr,
text=instance_attr.attr,
),
)
def _check_lambda_attribute(self, node: ast.Lambda) -> None:
assigned = walk.get_closest_parent(node, AssignNodes)
if not assigned or not isinstance(assigned, ast.Assign):
return # just used, not assigned
context = nodes.get_context(assigned)
if not isinstance(context, types.AnyFunctionDef) or not isinstance(
nodes.get_context(context),
ast.ClassDef,
):
return # it is not assigned in a method of a class
for attribute in assigned.targets:
if isinstance(
attribute, ast.Attribute
) and attributes.is_special_attr(attribute):
self.add_violation(oop.LambdaAttributeAssignedViolation(node))
@final
class ClassMethodOrderVisitor(base.BaseNodeVisitor):
"""Checks that all methods inside the class are ordered correctly."""
def visit_ClassDef(self, node: ast.ClassDef) -> None:
"""Ensures that class has correct methods order."""
self._check_method_order(node)
self.generic_visit(node)
def _check_method_order(self, node: ast.ClassDef) -> None:
method_nodes = [
subnode.name
for subnode in ast.walk(node)
if (
isinstance(subnode, FunctionNodes)
and nodes.get_context(subnode) is node
)
]
ideal = sorted(method_nodes, key=self._ideal_order, reverse=True)
for existing_order, ideal_order in zip(
method_nodes,
ideal,
strict=False,
):
if existing_order != ideal_order:
self.add_violation(consistency.WrongMethodOrderViolation(node))
return
def _ideal_order(self, first: str) -> int:
base_methods_order = {
'__init_subclass__': 7, # highest priority
'__new__': 6,
'__init__': 5,
'__call__': 4,
'__await__': 3,
}
public_and_magic_methods_priority = 2
if access.is_protected(first):
return 1
if access.is_private(first):
return 0 # lowest priority
return base_methods_order.get(first, public_and_magic_methods_priority)
@final
class BuggySuperCallVisitor(base.BaseNodeVisitor):
"""
Responsible for finding wrong form of `super()` call for certain contexts.
Call to `super()` without arguments will cause unexpected `TypeError` in a
number of specific contexts. Read more: https://bugs.python.org/issue46175
"""
_buggy_super_contexts: ClassVar[types.AnyNodes] = (
ast.GeneratorExp,
ast.SetComp,
ast.ListComp,
ast.DictComp,
)
def visit_Call(self, node: ast.Call) -> None:
"""Checks if this is a `super()` call in a specific context."""
self._check_buggy_super_context(node)
self.generic_visit(node)
def _check_buggy_super_context(self, node: ast.Call):
if not isinstance(node.func, ast.Name):
return
if node.func.id != 'super' or node.args:
return
# Check for being in a nested function
ctx = nodes.get_context(node)
if isinstance(ctx, FunctionNodes):
outer_ctx = nodes.get_context(ctx)
if isinstance(outer_ctx, FunctionNodes):
self.add_violation(oop.BuggySuperContextViolation(node))
return
if walk.get_closest_parent(node, self._buggy_super_contexts):
self.add_violation(oop.BuggySuperContextViolation(node))

Sorry, the diff of this file is too big to display