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.1.0
to
1.2.0
+34
wemake_python_styl...itors/ast/complexity/complex_finally.py
import ast
from typing import final
from wemake_python_styleguide.violations import complexity
from wemake_python_styleguide.visitors.base import BaseNodeVisitor
@final
class ComplexFinallyBlocksVisitor(BaseNodeVisitor):
"""Ensures there are no complex ``finally`` blocks."""
def visit_Try(self, node: ast.Try) -> None:
"""Visits all finally nodes in the tree."""
self._check_complex_finally(node)
self.generic_visit(node)
def _check_complex_finally(self, node: ast.Try) -> None:
"""Checks complexity of finally blocks."""
if not node.finalbody:
return
first_line = node.finalbody[0].lineno
# `end_lineno` was added in 3.8, but typing is not really correct,
# we are pretty sure that it always exist in modern python versions.
last_line = getattr(node.finalbody[-1], 'end_lineno', 0) or 0
total_lines = last_line - first_line + 1
if total_lines > self.options.max_lines_in_finally:
self.add_violation(
complexity.ComplexFinallyViolation(
node,
text=str(total_lines),
baseline=self.options.max_lines_in_finally,
),
)
+3
-9
Metadata-Version: 2.3
Name: wemake-python-styleguide
Version: 1.1.0
Version: 1.2.0
Summary: The strictest and most opinionated python linter ever

@@ -14,9 +14,3 @@ License: MIT

Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules

@@ -26,4 +20,4 @@ Classifier: Topic :: Software Development :: Quality Assurance

Requires-Dist: attrs
Requires-Dist: flake8 (>=7.1,<8.0)
Requires-Dist: pygments (>=2.5,<3.0)
Requires-Dist: flake8 (>=7.3,<8.0)
Requires-Dist: pygments (>=2.19,<3.0)
Project-URL: Funding, https://opencollective.com/wemake-python-styleguide

@@ -30,0 +24,0 @@ Project-URL: Homepage, https://wemake-python-styleguide.rtfd.io

@@ -5,11 +5,11 @@ [build-system]

[tool.poetry]
[project]
name = "wemake-python-styleguide"
version = "1.1.0"
version = "1.2.0"
description = "The strictest and most opinionated python linter ever"
license = "MIT"
license = {text = "MIT"}
authors = [
"Nikita Sobolev <mail@sobolevn.me>",
{name = "Nikita Sobolev", email = "mail@sobolevn.me"}
]

@@ -19,5 +19,2 @@

repository = "https://github.com/wemake-services/wemake-python-styleguide"
homepage = "https://wemake-python-styleguide.rtfd.io"
keywords = [

@@ -45,4 +42,6 @@ "flake8",

[tool.poetry.urls]
"Funding" = "https://opencollective.com/wemake-python-styleguide"
[project.urls]
Homepage = "https://wemake-python-styleguide.rtfd.io"
Repository = "https://github.com/wemake-services/wemake-python-styleguide"
Funding = "https://opencollective.com/wemake-python-styleguide"

@@ -58,18 +57,18 @@ [tool.poetry.plugins."flake8.extension"]

flake8 = "^7.1"
flake8 = "^7.3"
attrs = "*"
pygments = "^2.5"
pygments = "^2.19"
[tool.poetry.group.dev.dependencies]
pytest = "^8.1"
pytest-cov = "^6.0"
pytest = "^8.4"
pytest-cov = "^6.2"
pytest-randomly = "^3.12"
pytest-xdist = "^3.6"
pytest-xdist = "^3.8"
covdefaults = "^2.3"
syrupy = "^4.6"
hypothesis = "^6.35"
hypothesis = "^6.135"
hypothesmith = "^0.3"
mypy = "^1.13"
types-flake8 = "^7.1"
mypy = "^1.16"
types-flake8 = "^7.3"

@@ -79,5 +78,5 @@ import-linter = "^2.0"

astpath = "^0.9"
lxml = "^5.1"
lxml = "^6.0"
nbqa = "^1.2"
ruff = "^0.11"
ruff = "^0.12"
black = "^25.1"

@@ -123,2 +122,3 @@

quote-style = "single"
# This is only required because we have invalid on-purpose code in docstrings:
docstring-code-format = false

@@ -125,0 +125,0 @@

@@ -11,2 +11,3 @@ """

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

@@ -48,1 +49,28 @@

return 1
def check_is_node_in_specific_annotation(
node: ast.AST | None,
annotation_name: str,
annotation_modules: Container[str],
) -> bool:
"""
Check is node inside specific annotation.
Checks is ast node in annotation with name `annotation_name`
and is annotation module in `annotation_modules`.
"""
if isinstance(node, ast.Subscript):
if (
isinstance(node.value, ast.Attribute)
and isinstance(node.value.value, ast.Name)
and node.value.value.id in annotation_modules
and node.value.attr == annotation_name
):
return True
if (
isinstance(node.value, ast.Name)
and node.value.id in annotation_name
):
return True
return False

@@ -7,3 +7,12 @@ import ast

_ENUM_NAMES: Final = (
_CONCRETE_ENUM_NAMES: Final = (
'enum.StrEnum',
'StrEnum',
'enum.IntEnum',
'IntEnum',
'enum.IntFlag',
'IntFlag',
)
_REGULAR_ENUM_NAMES: Final = (
'enum.Enum',

@@ -15,4 +24,13 @@ 'enum.EnumType',

'EnumMeta',
'enum.Flag',
'Flag',
'enum.ReprEnum',
'ReprEnum',
)
_ENUM_NAMES: Final = (
*_CONCRETE_ENUM_NAMES,
*_REGULAR_ENUM_NAMES,
)
_ENUM_LIKE_NAMES: Final = (

@@ -37,7 +55,11 @@ *_ENUM_NAMES,

def has_enum_base(defn: ast.ClassDef) -> bool:
"""Tells whether some class has `Enum` or similar class as its base."""
return _has_one_of_base_classes(defn, _ENUM_NAMES)
def has_regular_enum_base(defn: ast.ClassDef) -> bool:
"""Tells whether some class has `Enum` or similar class as its base.
Excluded `IntEnum`, `StrEnum`, `IntFlag` concrete `Enum` subclasses.
Because those classes have already been subclassed using primitives.
"""
return _has_one_of_base_classes(defn, _REGULAR_ENUM_NAMES)
def has_enum_like_base(defn: ast.ClassDef) -> bool:

@@ -44,0 +66,0 @@ """

@@ -158,2 +158,5 @@ """

:str:`wemake_python_styleguide.options.defaults.MAX_MATCH_CASES`
- ``max-lines-in-finally`` - maximum lines in finally block of code
defaults to
:str:`wemake_python_styleguide.options.defaults.MAX_LINES_IN_FINALLY`

@@ -435,2 +438,7 @@ .. rubric:: Formatter options

),
_Option(
'--max-lines-in-finally',
defaults.MAX_LINES_IN_FINALLY,
'Maximum lines of expressions in a finally block.',
),
# Formatter:

@@ -437,0 +445,0 @@ _Option(

@@ -59,2 +59,5 @@ """

#: Maximum lines of expressions in a `finally` block.
MAX_LINES_IN_FINALLY: Final = 2 # guessed
#: Maximum number of `return` statements allowed in a single function.

@@ -61,0 +64,0 @@ MAX_RETURNS: Final = 5 # 7-2

@@ -104,2 +104,3 @@ from typing import Any, final

max_match_cases: int = attr.ib(validator=[_min_max(min=1)])
max_lines_in_finally: int = attr.ib(validator=[_min_max(min=1)])
show_violation_links: bool

@@ -106,0 +107,0 @@ exps_for_one_empty_line: int

@@ -8,2 +8,3 @@ from typing import Final

classes,
complex_finally,
counts,

@@ -43,2 +44,3 @@ function,

pm.MatchCasesVisitor,
complex_finally.ComplexFinallyBlocksVisitor,
)

@@ -17,2 +17,3 @@ from typing import Final

comments.EmptyCommentVisitor,
comments.CommentInFormattedStringVisitor,
syntax.WrongKeywordTokenVisitor,

@@ -23,2 +24,3 @@ primitives.WrongNumberTokenVisitor,

conditions.IfElseVisitor,
primitives.MultilineFormattedStringTokenVisitor,
)

@@ -77,2 +77,3 @@ from typing import Final

subscripts.CorrectKeyVisitor,
subscripts.StrictSliceOperations,
decorators.WrongDecoratorVisitor,

@@ -79,0 +80,0 @@ redundancy.RedundantEnumerateVisitor,

@@ -66,2 +66,3 @@ """

TooManyMatchCaseViolation
ComplexFinallyViolation

@@ -113,2 +114,3 @@ Module complexity

.. autoclass:: TooManyMatchCaseViolation
.. autoclass:: ComplexFinallyViolation

@@ -1402,1 +1404,30 @@ """

code = 242
@final
class ComplexFinallyViolation(ASTViolation):
"""
Forbids complex ``finally`` block.
Reasoning:
``finally`` is very special. It executes code in all
cases and therefore can't fail. When there are many lines in ``finally``
it indicates a larger problem: brittle and complex cleanups.
Solution:
Simplify the ``finally`` block. Use context managers, use ``ExitStack``.
Configuration:
This rule is configurable with ``--max-lines-in-finally``.
Default:
:str:`wemake_python_styleguide.options.defaults.MAX_LINES_IN_FINALLY`
See also:
https://peps.python.org/pep-0765
.. versionadded:: 1.2.0
"""
error_template = 'Found too many lines in `finally` block: {0}'
code = 243

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

from wemake_python_styleguide.logic import nodes, source, walk
from wemake_python_styleguide.logic.complexity.annotations import (
check_is_node_in_specific_annotation,
)
from wemake_python_styleguide.logic.tree import (

@@ -184,2 +187,8 @@ attributes,

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

@@ -194,11 +203,16 @@ def visit_Num(self, node: ast.Constant) -> None:

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

@@ -205,0 +219,0 @@ token = self._token_dict[node.lineno, node.col_offset]

@@ -80,3 +80,3 @@ import ast

id_attr,
) and not enums.has_enum_base(node):
) and not enums.has_regular_enum_base(node):
self.add_violation(

@@ -83,0 +83,0 @@ oop.BuiltinSubclassViolation(node, text=id_attr),

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

def _update_counters(self, node: AnyFunctionDef, sub_node: ast.AST) -> None:
"""Updates statement counters for the given node."""
error_counters: _NodeTypeHandler = {
ast.Return: self.metrics.returns,
ast.Expr: self.metrics.expressions,
ast.Await: self.metrics.awaits,
ast.Assert: self.metrics.asserts,
ast.Raise: self.metrics.raises,
}
for types, counter in error_counters.items():
if isinstance(sub_node, types):
# We do not count returns in nested functions and classes
if types is ast.Return and get_context(sub_node) is not node:
continue
counter[node] += 1
def _check_sub_node(

@@ -103,15 +120,5 @@ self,

error_counters: _NodeTypeHandler = {
ast.Return: self.metrics.returns,
ast.Expr: self.metrics.expressions,
ast.Await: self.metrics.awaits,
ast.Assert: self.metrics.asserts,
ast.Raise: self.metrics.raises,
}
self._update_counters(node, sub_node)
for types, counter in error_counters.items():
if isinstance(sub_node, types):
counter[node] += 1
@final

@@ -118,0 +125,0 @@ @alias(

import ast
from typing import Final, final
from wemake_python_styleguide.compat.constants import PY312
from wemake_python_styleguide.logic.tree import attributes
from wemake_python_styleguide.options.validation import ValidatedOptions
from wemake_python_styleguide.types import AnyFunctionDef

@@ -18,3 +20,8 @@ from wemake_python_styleguide.violations.best_practices import (

_ALLOWED_DECORATOR_TYPES3_12: Final = (
*_ALLOWED_DECORATOR_TYPES,
ast.Subscript, # PEP 695 - Type Parameter Syntax
)
@final

@@ -31,2 +38,14 @@ @alias(

def __init__(
self,
options: ValidatedOptions,
tree: ast.AST,
**kwargs,
) -> None:
"""Creates Decorator Visitor."""
self.ALLOWED_DECORATOR_TYPES: Final = (
_ALLOWED_DECORATOR_TYPES3_12 if PY312 else _ALLOWED_DECORATOR_TYPES
)
super().__init__(options, tree, **kwargs)
def visit_any_function(self, node: AnyFunctionDef) -> None:

@@ -43,3 +62,3 @@ """Checks functions' decorators."""

def _is_allowed_decorator(self, node: ast.expr) -> bool:
if not isinstance(node, _ALLOWED_DECORATOR_TYPES):
if not isinstance(node, self.ALLOWED_DECORATOR_TYPES):
return False

@@ -50,5 +69,4 @@

return all(
isinstance(part, _ALLOWED_DECORATOR_TYPES)
for part in attributes.parts(node)
return attributes.only_consists_of_parts(
node, self.ALLOWED_DECORATOR_TYPES
)

@@ -258,10 +258,31 @@ import ast

def _is_node_in_loop_iter(self, node: ast.AST, loop_iter: ast.expr) -> bool:
return walk.is_contained_by(node, loop_iter) or node is loop_iter
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
if node_parent is None:
return
if isinstance(node_parent, AnyComprehension):
if all(
comprehension.is_async
for comprehension in node_parent.generators
):
# async comprehensions are allowed to use `await`
return
if any(
self._is_node_in_loop_iter(node, gen.iter)
for gen in node_parent.generators
):
return
if isinstance(node_parent, ast.For) and self._is_node_in_loop_iter(
node, node_parent.iter
):
# async comprehensions are allowed to use `await`
# await allowed in loop definition
return
if node_parent is not None:
self.add_violation(AwaitInLoopViolation(node))
self.add_violation(AwaitInLoopViolation(node))

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

indexes: list[ast.expr | None] = []
lower_ok = node.slice.lower is None or (

@@ -65,7 +66,5 @@ not self._is_zero(node.slice.lower)

)
upper_ok = node.slice.upper is None or not self._is_none(
node.slice.upper,
)
step_ok = node.slice.step is None or (

@@ -76,9 +75,14 @@ not self._is_one(node.slice.step)

if not (lower_ok and upper_ok and step_ok):
self.add_violation(
consistency.RedundantSubscriptViolation(
node,
),
)
if not lower_ok:
indexes.append(node.slice.lower)
if not upper_ok:
indexes.append(node.slice.upper)
if not step_ok:
indexes.append(node.slice.step)
for index in indexes:
self.add_violation(consistency.RedundantSubscriptViolation(index))
def _check_slice_assignment(self, node: ast.Subscript) -> None:

@@ -188,1 +192,67 @@ if not isinstance(node.ctx, ast.Store):

)
@final
class StrictSliceOperations(base.BaseNodeVisitor):
"""Check for stricter operation with slices."""
def visit_Slice(self, node: ast.Slice) -> None:
"""Visit slice."""
self._check_reverse(node)
self._check_copy(node)
self.generic_visit(node)
def _check_reverse(self, node: ast.Slice) -> None:
if not (
self._is_node_or_none(node.lower)
or self._is_node_have_value(node.lower, value_to_check=-1)
):
return
if not (
self._is_node_or_none(node.upper)
and self._is_node_have_value(node.step, value_to_check=-1)
):
return
self.add_violation(
best_practices.NonStrictSliceOperationsViolation(node)
)
def _check_copy(self, node: ast.Slice) -> None:
if not (
self._is_node_or_none(node.lower)
or self._is_node_have_value(node.lower, value_to_check=0)
):
return
if not self._is_node_or_none(node.upper):
return
if not (
self._is_node_or_none(node.step)
or self._is_node_have_value(node.step, value_to_check=1)
):
return
self.add_violation(
best_practices.NonStrictSliceOperationsViolation(node)
)
def _is_node_or_none(self, node: ast.AST | None) -> bool:
return node is None or (
isinstance(node, ast.Constant) and node.value is None
)
def _is_node_have_value(
self, node: ast.AST | None, value_to_check: int
) -> bool:
if value_to_check < 0:
return (
isinstance(node, ast.UnaryOp)
and isinstance(node.op, ast.USub)
and isinstance(node.operand, ast.Constant)
and node.operand.value == abs(value_to_check)
)
return isinstance(node, ast.Constant) and node.value == value_to_check

@@ -29,2 +29,3 @@ r"""

from wemake_python_styleguide.violations.best_practices import (
CommentInFormattedStringViolation,
EmptyCommentViolation,

@@ -315,1 +316,31 @@ ForbiddenInlineIgnoreViolation,

)
@final
class CommentInFormattedStringVisitor(BaseTokenVisitor): # pragma: >=3.12 cover
"""Checks comment in formatted strings."""
_comment_in_fstring: ClassVar[re.Pattern[str]] = re.compile(
r"""
.* # (1) anything before the f-string
fr?(['"]) # (2) `f` or `fr`prefix + a single or double quote
.* # (3) any characters up to…
\{ # (4) opening brace
[^}]* # (5) any characters except closing braces
# # (6) hash symbol
[^}]*\n # (7) chars up to a newline (i.e. multiline)
""",
re.VERBOSE,
)
def visit_fstring_start(self, token: tokenize.TokenInfo) -> None:
"""Preforms fstring check."""
self._check_is_fstring_ends_with_comment(token)
def _check_is_fstring_ends_with_comment(
self,
token: tokenize.TokenInfo,
) -> None:
"""Checks is formatted string ends with comment."""
if self._comment_in_fstring.match(token.line):
self.add_violation(CommentInFormattedStringViolation(token))

@@ -15,2 +15,3 @@ import re

from wemake_python_styleguide.violations.best_practices import (
MultilineFormattedStringViolation,
WrongUnicodeEscapeViolation,

@@ -230,1 +231,31 @@ )

self._checker.check_string_modifiers(token, modifiers)
@final
class MultilineFormattedStringTokenVisitor(
BaseTokenVisitor
): # pragma: >=3.12 cover
"""Checks incorrect formatted string usages."""
_multiline_fstring_pattern: ClassVar[re.Pattern[str]] = re.compile(
r"""
.* # (1) anything before the f-string
fr?(['"]) # (2) `f` or `fr`prefix + a single or double quote
(?!\1\1) # (3) not triple quote
.* # (4) any characters up to…
(\{.*\}.)* # (5) any fully closed {…} expressions, if present
.* # (6) then more arbitrary chars
\{ # (7) an opening brace of an f-expr
[^}]*\n # (8) chars up to a newline (i.e. multiline)
""",
re.VERBOSE,
)
def visit_fstring_start(self, token: tokenize.TokenInfo) -> None:
"""Performs check."""
self._check_fstring_is_multi_lined(token)
def _check_fstring_is_multi_lined(self, token: tokenize.TokenInfo) -> None:
"""Finds if f-string is multi-line."""
if self._multiline_fstring_pattern.match(token.line):
self.add_violation(MultilineFormattedStringViolation(token))

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