wemake-python-styleguide
Advanced tools
| 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 | ||
| [](https://opencollective.com/wemake-python-styleguide) | ||
| [](https://github.com/wemake-services/wemake-python-styleguide/actions?query=workflow%3Atest) | ||
| [](https://github.com/wemake-services/wemake-python-styleguide/actions/workflows/test.yml) | ||
| [](https://codecov.io/gh/wemake-services/wemake-python-styleguide) | ||
@@ -126,3 +126,3 @@ [](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: |
+35
-24
| [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" |
+2
-2
@@ -5,3 +5,3 @@ # wemake-python-styleguide | ||
| [](https://opencollective.com/wemake-python-styleguide) | ||
| [](https://github.com/wemake-services/wemake-python-styleguide/actions?query=workflow%3Atest) | ||
| [](https://github.com/wemake-services/wemake-python-styleguide/actions/workflows/test.yml) | ||
| [](https://codecov.io/gh/wemake-services/wemake-python-styleguide) | ||
@@ -94,3 +94,3 @@ [](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
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
674925
2.51%160
9.59%17748
2.58%