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
66
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
0.19.2
to
1.0.0
+15
wemake_python_styleguide/logic/arguments/special_args.py
import ast
from wemake_python_styleguide import constants, types
def clean_special_argument(
node: types.AnyFunctionDefAndLambda,
node_args: list[ast.arg],
) -> list[ast.arg]:
"""Removes ``self``, ``cls``, ``mcs`` from argument lists."""
if not node_args or isinstance(node, ast.Lambda):
return node_args
if node_args[0].arg not in constants.SPECIAL_ARGUMENT_NAMES_WHITELIST:
return node_args
return node_args[1:]
from functools import reduce
def get_duplicate_names(variables: list[set[str]]) -> set[str]:
"""
Find duplicate names in different nodes.
>>> get_duplicate_names([{'a', 'b'}, {'b', 'c'}])
{'b'}
"""
return reduce(
lambda acc, element: acc.intersection(element),
variables,
)
import ast
from typing import Final
from wemake_python_styleguide.logic.source import node_to_string
_ENUM_NAMES: Final = (
'enum.Enum',
'enum.EnumType',
'enum.EnumMeta',
'Enum',
'EnumType',
'EnumMeta',
)
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)
import re
from typing import Final
_UNDERSCORE_PATTERN: Final = re.compile(r'^\d{1,3}(_\d{3})*$')
_SPLIT_PATTERN: Final = re.compile(r'\.|e[\+-]?')
def has_correct_underscores(number: str) -> bool:
"""
Formats a number as a string separated by thousands with support floating.
>>> has_correct_underscores('1_234.157_000e-1_123')
True
>>> has_correct_underscores('0b1_001')
True
>>> has_correct_underscores('12_345.987_654_321')
True
>>> has_correct_underscores('10000_000_00')
False
"""
assert '_' in number # noqa: S101
number_cleared = (
number.strip()
.lower()
.removeprefix('0b')
.removeprefix('0x')
.removeprefix('0o')
.removesuffix('j')
)
return all(
_UNDERSCORE_PATTERN.match(number_part)
for number_part in _SPLIT_PATTERN.split(number_cleared)
)
import ast
from typing import final
from wemake_python_styleguide.violations import complexity
from wemake_python_styleguide.visitors.base import BaseNodeVisitor
@final
class MatchSubjectsVisitor(BaseNodeVisitor):
"""Finds excessive match subjects in `match` statements."""
def visit_Match(self, node: ast.Match) -> None:
"""Finds all `match` statements and checks their subjects."""
self._check_match_subjects_count(node)
self.generic_visit(node)
def _check_match_subjects_count(self, node: ast.Match) -> None:
if not isinstance(node.subject, ast.Tuple):
return
if len(node.subject.elts) <= self.options.max_match_subjects:
return
self.add_violation(
complexity.TooManyMatchSubjectsViolation(
node,
text=str(len(node.subject.elts)),
baseline=self.options.max_match_subjects,
)
)
@final
class MatchCasesVisitor(BaseNodeVisitor):
"""Finds excessive match cases in `match` statements."""
def visit_Match(self, node: ast.Match) -> None:
"""Finds all `match` statements and checks their cases."""
self._check_match_cases_count(node)
self.generic_visit(node)
def _check_match_cases_count(self, node: ast.Match) -> None:
if len(node.cases) > self.options.max_match_cases:
self.add_violation(
violation=complexity.TooManyMatchCaseViolation(
text=str(len(node.cases)),
node=node,
baseline=self.options.max_match_cases,
),
)
import ast
from typing import ClassVar, final
from wemake_python_styleguide.logic.tree import pattern_matching
from wemake_python_styleguide.types import AnyNodes
from wemake_python_styleguide.violations.refactoring import (
ExtraMatchSubjectSyntaxViolation,
)
from wemake_python_styleguide.visitors.base import BaseNodeVisitor
@final
class MatchSubjectVisitor(BaseNodeVisitor):
"""Restricts the incorrect subjects in PM."""
_forbidden_syntax: ClassVar[AnyNodes] = (
ast.Dict,
ast.Set,
ast.List,
ast.Tuple,
)
def visit_Match(self, node: ast.Match) -> None:
"""Visits `Match` nodes and checks their internals."""
self._check_extra_syntax(node.subject)
self.generic_visit(node)
def _check_extra_syntax(self, node: ast.expr) -> None:
if not isinstance(node, self._forbidden_syntax):
return
if pattern_matching.is_constant_subject(node):
return # raises another violation in a different place
if isinstance(node, ast.Tuple) and len(node.elts) > 1:
return
self.add_violation(ExtraMatchSubjectSyntaxViolation(node))
+28
-36
Metadata-Version: 2.1
Name: wemake-python-styleguide
Version: 0.19.2
Version: 1.0.0
Summary: The strictest and most opinionated python linter ever

@@ -10,4 +10,4 @@ Home-page: https://wemake-python-styleguide.rtfd.io

Author-email: mail@sobolevn.me
Requires-Python: >=3.9,<4.0
Classifier: Development Status :: 4 - Beta
Requires-Python: >=3.10,<4.0
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Console

@@ -19,29 +19,12 @@ Classifier: Framework :: Flake8

Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
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
Classifier: Topic :: Software Development :: Quality Assurance
Classifier: Typing :: Typed
Requires-Dist: astor (>=0.8,<0.9)
Requires-Dist: attrs
Requires-Dist: darglint (>=1.2,<2.0)
Requires-Dist: flake8 (>=7.0,<8.0)
Requires-Dist: flake8-bandit (>=4.1,<5.0)
Requires-Dist: flake8-broken-line (>=1.0,<2.0)
Requires-Dist: flake8-bugbear (>=24.2,<25.0)
Requires-Dist: flake8-commas (>=2.0,<3.0)
Requires-Dist: flake8-comprehensions (>=3.1,<4.0)
Requires-Dist: flake8-debugger (>=4.0,<5.0)
Requires-Dist: flake8-docstrings (>=1.3,<2.0)
Requires-Dist: flake8-eradicate (>=1.5,<2.0)
Requires-Dist: flake8-isort (>=6.0,<7.0)
Requires-Dist: flake8-quotes (>=3.0,<4.0)
Requires-Dist: flake8-rst-docstrings (>=0.3,<0.4)
Requires-Dist: flake8-string-format (>=0.3,<0.4)
Requires-Dist: pep8-naming (>=0.13,<0.14)
Requires-Dist: flake8 (>=7.1,<8.0)
Requires-Dist: pygments (>=2.4,<3.0)
Requires-Dist: setuptools
Requires-Dist: typing_extensions (>=4.0,<5.0)
Project-URL: Funding, https://opencollective.com/wemake-python-styleguide

@@ -72,4 +55,5 @@ Project-URL: Repository, https://github.com/wemake-services/wemake-python-styleguide

`wemake-python-styleguide` is actually a [flake8](http://flake8.pycqa.org/en/latest/)
plugin with [some other plugins](https://wemake-python-styleguide.rtfd.io/en/latest/pages/usage/violations/index.html#external-plugins) as dependencies.
plugin, the only one you will need as your [ruff](https://github.com/astral-sh/ruff) companion.
Fully compatible with **ALL** rules and format conventions from `ruff`.

@@ -88,4 +72,3 @@ ## Quickstart

- [flakeheaven](https://wemake-python-styleguide.rtfd.io/en/latest/pages/usage/integrations/flakeheaven.html) for easy integration into a **legacy** codebase
- [nitpick](https://wemake-python-styleguide.rtfd.io/en/latest/pages/usage/integrations/nitpick.html) for sharing and validating configuration across multiple projects
- [ondivi](https://wemake-python-styleguide.rtfd.io/en/latest/pages/usage/integrations/ondivi.html) for easy integration into a **legacy** codebase

@@ -96,3 +79,3 @@

```bash
flake8 your_module.py
flake8 your_module.py --select=WPS
```

@@ -114,3 +97,12 @@

Can (and should!) be used with `ruff`:
```bash
ruff check && ruff format
flake8 . --select=WPS
```
See example `ruff` configuration in our [`pyproject.toml`](https://github.com/wemake-services/wemake-python-styleguide/blob/master/pyproject.toml#L103).
## Strict is the new cool

@@ -131,10 +123,10 @@

| | flake8 | pylint | black | mypy | wemake-python-styleguide |
|----------------------------|--------|--------|-------|------|--------------------------|
| Formats code? | ❌ | ❌ | ✅ | ❌ | ❌ |
| Finds style issues? | 🤔 | ✅ | 🤔 | ❌ | ✅ |
| Finds bugs? | 🤔 | ✅ | ❌ | ✅ | ✅ |
| Finds complex code? | ❌ | 🤔 | ❌ | ❌ | ✅ |
| Has a lot of strict rules? | ❌ | 🤔 | ❌ | ❌ | ✅ |
| Has a lot of plugins? | ✅ | ❌ | ❌ | 🤔 | ✅ |
| | flake8 | pylint | black | mypy | ruff | wemake-python-styleguide |
|----------------------------|--------|--------|-------|------|------|--------------------------|
| Formats code? | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ |
| Finds style issues? | 🤔 | ✅ | 🤔 | ❌ | ✅ | ❌ |
| Finds bugs? | 🤔 | ✅ | ❌ | ✅ | ✅ | ✅ |
| Finds complex code? | ❌ | 🤔 | ❌ | ❌ | ✅ | ✅ |
| Has a lot of strict rules? | ❌ | 🤔 | ❌ | ❌ | ✅ | ✅ |
| Has a lot of plugins? | ✅ | ❌ | ❌ | 🤔 | ❌ | ✅ |

@@ -155,3 +147,3 @@ We have several primary objectives:

0. Assume or check types, use `mypy` together with our linter
1. [Reformat code](https://wemake-python-styleguide.rtfd.io/en/latest/pages/usage/integrations/auto-formatters.html), since we believe that developers should do that
1. Format code or produce stylistic errors, use `ruff format` for that
2. Check for `SyntaxError` or logical bugs, write tests instead

@@ -163,3 +155,3 @@ 3. Appeal to everyone. But, you can [switch off](https://wemake-python-styleguide.rtfd.io/en/latest/pages/usage/setup.html#ignoring-violations) any rules that you don't like

We in [wemake.services](https://wemake.services) make
We in [wemake.services](https://github.com/wemake-services) make
all our tools open-source by default, so the community can benefit from them.

@@ -166,0 +158,0 @@ If you use our tools and they make your life easier and brings business value,

@@ -0,4 +1,8 @@

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

@@ -9,3 +13,3 @@

authors = [
"Nikita Sobolev <mail@sobolevn.me>"
"Nikita Sobolev <mail@sobolevn.me>",
]

@@ -26,7 +30,7 @@

"code quality",
"pycqa"
"pycqa",
]
classifiers = [
"Development Status :: 4 - Beta",
"Development Status :: 5 - Production/Stable",
"Environment :: Console",

@@ -51,34 +55,14 @@ "Framework :: Flake8",

[tool.poetry.dependencies]
python = "^3.9"
python = "^3.10"
flake8 = "^7.0"
flake8 = "^7.1"
attrs = "*"
setuptools = "*" # only needed for flake8-commas
typing_extensions = ">=4.0,<5.0"
astor = "^0.8"
pygments = "^2.4"
flake8-commas = "^2.0"
flake8-quotes = "^3.0"
flake8-comprehensions = "^3.1"
flake8-docstrings = "^1.3"
flake8-string-format = "^0.3"
flake8-bugbear = "^24.2"
flake8-debugger = "^4.0"
flake8-isort = "^6.0"
flake8-eradicate = "^1.5"
flake8-bandit = "^4.1"
flake8-broken-line = "^1.0"
flake8-rst-docstrings = "^0.3"
pep8-naming = "^0.13"
darglint = "^1.2"
[tool.poetry.group.dev.dependencies]
nitpick = "^0.35"
flake8-pytest-style = "^1.5"
pytest = "^8.1"
pytest-cov = "^5.0"
pytest-cov = "^6.0"
pytest-randomly = "^3.12"
coverage-conditional-plugin = "^0.9"
pytest-xdist = "^3.6"
covdefaults = "^2.3"
syrupy = "^4.6"

@@ -88,5 +72,5 @@ hypothesis = "^6.35"

mypy = "^1.9"
mypy = "^1.13"
types-flake8 = "^7.1"
autopep8 = "^2.0"
import-linter = "^2.0"

@@ -97,3 +81,4 @@

nbqa = "^1.2"
doc8 = "^1.1"
ruff = "^0.8"
black = "^24.10"

@@ -104,16 +89,186 @@ [tool.poetry.group.docs]

[tool.poetry.group.docs.dependencies]
sphinx = "^7.1"
sphinx = "^8.1"
sphinx-autodoc-typehints = "^2.0"
sphinxcontrib-mermaid = "^0.9"
sphinxcontrib-mermaid = "^1.0"
added-value = "^0.24"
m2r2 = "^0.3"
tomli = "^2.0"
myst-parser = "^4.0"
[tool.black]
line-length = 80
preview = true
skip-string-normalization = true # we use '
target-version = [ 'py310' ]
# Exclude intentionally bad files:
extend-exclude = '''
(
tests/.*/__snapshots__/.* | tests/fixtures/.*
)
'''
[build-system]
requires = ["poetry-core>=1.9.0"]
build-backend = "poetry.core.masonry.api"
[tool.ruff]
# Ruff config: https://docs.astral.sh/ruff/settings
target-version = "py310"
line-length = 80
extend-exclude = [
# Intentionally bad code:
"tests/**/__snapshots__/**",
"tests/fixtures/**",
]
preview = true
fix = true
format.quote-style = "single"
format.docstring-code-format = false
lint.select = [
"A", # flake8-builtins
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"C90", # maccabe
"COM", # flake8-commas
"D", # pydocstyle
"DTZ", # flake8-datetimez
"E", # pycodestyle
"ERA", # flake8-eradicate
"EXE", # flake8-executable
"F", # pyflakes
"FBT", # flake8-boolean-trap
"FLY", # pyflint
"FURB", # refurb
"G", # flake8-logging-format
"I", # isort
"ICN", # flake8-import-conventions
"ISC", # flake8-implicit-str-concat
"LOG", # flake8-logging
"N", # pep8-naming
"PERF", # perflint
"PIE", # flake8-pie
"PL", # pylint
"PT", # flake8-pytest-style
"PTH", # flake8-use-pathlib
"Q", # flake8-quotes
"RET", # flake8-return
"RSE", # flake8-raise
"RUF", # ruff
"S", # flake8-bandit
"SIM", # flake8-simpify
"SLF", # flake8-self
"SLOT", # flake8-slots
"T100", # flake8-debugger
"TRY", # tryceratops
"UP", # pyupgrade
"W", # pycodestyle
"YTT", # flake8-2020
]
lint.ignore = [
"A005", # allow to shadow stdlib and builtin module names
"COM812", # trailing comma, conflicts with `ruff format`
# Different doc rules that we don't really care about:
"D100",
"D104",
"D106",
"D203",
"D212",
"D401",
"D404",
"D405",
"ISC001", # implicit string concat conflicts with `ruff format`
"ISC003", # prefer explicit string concat over implicit concat
"PLR09", # we have our own complexity rules
"PLR2004", # do not report magic numbers
"PLR6301", # do not require classmethod / staticmethod when self not used
"TRY003", # long exception messages from `tryceratops`
]
lint.per-file-ignores."tests/*.py" = [
"S101", # asserts
"S105", # hardcoded passwords
"S404", # subprocess calls are for tests
"S603", # do not require `shell=True`
"S607", # partial executable paths
]
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"
[tool.nitpick]
style = "styles/nitpick-style-wemake.toml"
[tool.pytest.ini_options]
# pytest config: http://doc.pytest.org/en/latest/customize.html
# Strict `@xfail` by default:
xfail_strict = true
# Fail on warnings:
filterwarnings = [ "error" ]
addopts = [
"--strict",
"--doctest-modules",
# pytest-cov
"--cov=wemake_python_styleguide",
"--cov=tests",
"--cov-branch",
"--cov-report=term-missing:skip-covered",
"--cov-report=html",
"--cov-report=xml",
"--cov-fail-under=100",
# pytest-xdist
"-n=auto",
# Custom ignored dirs with bad code:
"--ignore=tests/fixtures",
"--ignore=docs",
]
[tool.coverage.run]
# Coverage configuration: https://coverage.readthedocs.io/
# We don't need to cover some files. They are fully checked with mypy.
# And don't contain any logic.
omit = [
# Does not contain runtime logic:
"wemake_python_styleguide/types.py",
# All version specific tests:
"tests/**/*312.py",
]
# Here we specify plugins for coverage to be used:
plugins = [
"covdefaults",
]
[tool.mypy]
# The mypy configurations: http://bit.ly/2zEl9WI
ignore_missing_imports = true
strict = true
local_partial_types = true
warn_unreachable = true
enable_error_code = [
"truthy-bool",
"truthy-iterable",
"redundant-expr",
"unused-awaitable",
# "ignore-without-code",
"possibly-undefined",
"redundant-self",
# "explicit-override",
# "mutable-override",
"unimported-reveal",
"deprecated",
]
disable_error_code = [
"no-untyped-def", # TODO: fix
]
[[tool.mypy.overrides]]
module = "wemake_python_styleguide.compat.nodes"
# We allow explicit `Any` only in this file, because of the compatibility:
disallow_any_explicit = false
[[tool.mypy.overrides]]
module = "wemake_python_styleguide.compat.packaging"
# We allow unused `ignore` comments, because we cannot sync it between versions:
warn_unused_ignores = false
+23
-14

@@ -22,4 +22,5 @@ # wemake-python-styleguide

`wemake-python-styleguide` is actually a [flake8](http://flake8.pycqa.org/en/latest/)
plugin with [some other plugins](https://wemake-python-styleguide.rtfd.io/en/latest/pages/usage/violations/index.html#external-plugins) as dependencies.
plugin, the only one you will need as your [ruff](https://github.com/astral-sh/ruff) companion.
Fully compatible with **ALL** rules and format conventions from `ruff`.

@@ -38,4 +39,3 @@ ## Quickstart

- [flakeheaven](https://wemake-python-styleguide.rtfd.io/en/latest/pages/usage/integrations/flakeheaven.html) for easy integration into a **legacy** codebase
- [nitpick](https://wemake-python-styleguide.rtfd.io/en/latest/pages/usage/integrations/nitpick.html) for sharing and validating configuration across multiple projects
- [ondivi](https://wemake-python-styleguide.rtfd.io/en/latest/pages/usage/integrations/ondivi.html) for easy integration into a **legacy** codebase

@@ -46,3 +46,3 @@

```bash
flake8 your_module.py
flake8 your_module.py --select=WPS
```

@@ -64,3 +64,12 @@

Can (and should!) be used with `ruff`:
```bash
ruff check && ruff format
flake8 . --select=WPS
```
See example `ruff` configuration in our [`pyproject.toml`](https://github.com/wemake-services/wemake-python-styleguide/blob/master/pyproject.toml#L103).
## Strict is the new cool

@@ -81,10 +90,10 @@

| | flake8 | pylint | black | mypy | wemake-python-styleguide |
|----------------------------|--------|--------|-------|------|--------------------------|
| Formats code? | ❌ | ❌ | ✅ | ❌ | ❌ |
| Finds style issues? | 🤔 | ✅ | 🤔 | ❌ | ✅ |
| Finds bugs? | 🤔 | ✅ | ❌ | ✅ | ✅ |
| Finds complex code? | ❌ | 🤔 | ❌ | ❌ | ✅ |
| Has a lot of strict rules? | ❌ | 🤔 | ❌ | ❌ | ✅ |
| Has a lot of plugins? | ✅ | ❌ | ❌ | 🤔 | ✅ |
| | flake8 | pylint | black | mypy | ruff | wemake-python-styleguide |
|----------------------------|--------|--------|-------|------|------|--------------------------|
| Formats code? | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ |
| Finds style issues? | 🤔 | ✅ | 🤔 | ❌ | ✅ | ❌ |
| Finds bugs? | 🤔 | ✅ | ❌ | ✅ | ✅ | ✅ |
| Finds complex code? | ❌ | 🤔 | ❌ | ❌ | ✅ | ✅ |
| Has a lot of strict rules? | ❌ | 🤔 | ❌ | ❌ | ✅ | ✅ |
| Has a lot of plugins? | ✅ | ❌ | ❌ | 🤔 | ❌ | ✅ |

@@ -105,3 +114,3 @@ We have several primary objectives:

0. Assume or check types, use `mypy` together with our linter
1. [Reformat code](https://wemake-python-styleguide.rtfd.io/en/latest/pages/usage/integrations/auto-formatters.html), since we believe that developers should do that
1. Format code or produce stylistic errors, use `ruff format` for that
2. Check for `SyntaxError` or logical bugs, write tests instead

@@ -113,3 +122,3 @@ 3. Appeal to everyone. But, you can [switch off](https://wemake-python-styleguide.rtfd.io/en/latest/pages/usage/setup.html#ignoring-violations) any rules that you don't like

We in [wemake.services](https://wemake.services) make
We in [wemake.services](https://github.com/wemake-services) make
all our tools open-source by default, so the community can benefit from them.

@@ -116,0 +125,0 @@ If you use our tools and they make your life easier and brings business value,

@@ -39,9 +39,11 @@ """

from __future__ import annotations
import ast
import tokenize
import traceback
from typing import ClassVar, Iterator, Sequence, Type
from collections.abc import Iterator, Sequence
from typing import TYPE_CHECKING, ClassVar, TypeAlias, final
from flake8.options.manager import OptionManager
from typing_extensions import TypeAlias, final

@@ -59,5 +61,8 @@ from wemake_python_styleguide import constants, types

VisitorClass: TypeAlias = Type[base.BaseVisitor]
if TYPE_CHECKING:
from wemake_python_styleguide.options.validation import ValidatedOptions
VisitorClass: TypeAlias = type[base.BaseVisitor]
@final

@@ -78,3 +83,3 @@ class Checker:

options: option structure passed by ``flake8``:
:class:`wemake_python_styleguide.types.ConfigurationOptions`.
:class:`wemake_python_styleguide.options.validation.ValidatedOptions`.

@@ -88,3 +93,3 @@ visitors: :term:`preset` of visitors that are run by this checker.

options: types.ConfigurationOptions
options: ValidatedOptions
config = Configuration()

@@ -139,3 +144,3 @@

@classmethod
def parse_options(cls, options: types.ConfigurationOptions) -> None:
def parse_options(cls, options: ValidatedOptions) -> None:
"""Parses registered options for providing them to each visitor."""

@@ -164,3 +169,3 @@ cls.options = validate_options(options)

# and some rules that still work.
print(traceback.format_exc()) # noqa: T001, WPS421
print(traceback.format_exc()) # noqa: WPS421
visitor.add_violation(system.InternalErrorViolation())

@@ -167,0 +172,0 @@

@@ -10,8 +10,21 @@ """

import ast
from typing import Final, final
from typing_extensions import Final
#: We need this tuple to easily work with both types of text nodes:
TextNodes: Final = (ast.Str, ast.Bytes)
@final
class _TextNodesMeta(type):
def __instancecheck__(cls, instance):
return isinstance(instance, ast.Constant) and isinstance(
instance.value,
str | bytes,
)
@final
class TextNodes(ast.AST, metaclass=_TextNodesMeta):
"""Check if node has type of `ast.Constant` with `str` or `bytes`."""
value: str | bytes # noqa: WPS110
#: We need this tuple to easily check that this is a real assign node.

@@ -18,0 +31,0 @@ AssignNodes: Final = (ast.Assign, ast.AnnAssign)

import sys
from typing import Final
from typing_extensions import Final
#: This indicates that we are running on python3.10+
PY310: Final = sys.version_info >= (3, 10)
#: This indicates that we are running on python3.11+

@@ -9,0 +5,0 @@ PY311: Final = sys.version_info >= (3, 11)

import ast
from typing import List, Tuple, Union

@@ -9,6 +8,6 @@ from wemake_python_styleguide.compat.types import NodeWithTypeParams

def get_assign_targets(
node: Union[AnyAssignWithWalrus, ast.AugAssign],
) -> List[ast.expr]:
node: AnyAssignWithWalrus | ast.AugAssign,
) -> list[ast.expr]:
"""Returns list of assign targets without knowing the type of assign."""
if isinstance(node, (ast.AnnAssign, ast.AugAssign, ast.NamedExpr)):
if isinstance(node, ast.AnnAssign | ast.AugAssign | ast.NamedExpr):
return [node.target]

@@ -18,5 +17,5 @@ return node.targets

def get_type_param_names( # pragma: py-lt-312
def get_type_param_names( # pragma: >=3.12 cover
node: NodeWithTypeParams,
) -> List[Tuple[ast.AST, str]]:
) -> list[tuple[ast.AST, str]]:
"""Return list of type parameters' names."""

@@ -23,0 +22,0 @@ type_params = []

@@ -10,30 +10,7 @@ """

import sys
from typing import Optional
if sys.version_info >= (3, 10): # pragma: py-lt-310
from ast import Match as Match
from ast import MatchAs as MatchAs
from ast import MatchStar as MatchStar
from ast import match_case as match_case
else: # pragma: py-gte-310
class Match(ast.stmt):
"""Used for ``match`` keyword and its body."""
if sys.version_info >= (3, 11): # pragma: >=3.11 cover
from ast import TryStar as TryStar
else: # pragma: <3.11 cover
class match_case(ast.AST): # noqa: N801
"""Used as a top level wrapper of pattern matched cases."""
class MatchAs(ast.AST):
"""Used to declare variables in pattern matched code."""
name: Optional[str] # noqa: WPS110
pattern: Optional[ast.AST]
class MatchStar(ast.AST):
"""Used to declare `[*rest]` and `{**rest}` patterns."""
name: Optional[str]
if sys.version_info >= (3, 11): # pragma: py-lt-311
from ast import TryStar as TryStar
else: # pragma: py-gte-311
class TryStar(ast.stmt):

@@ -47,5 +24,7 @@ """Used for `try/except*` statements."""

if sys.version_info >= (3, 12): # pragma: py-lt-312
if sys.version_info >= (3, 12): # pragma: >=3.12 cover
from ast import TypeAlias as TypeAlias
else: # pragma: py-gte-312
else: # pragma: <3.12 cover
class TypeAlias(ast.stmt):

@@ -56,1 +35,2 @@ """Used to define `TypeAlias` nodes in `python3.12+`."""

type_params: list[ast.stmt]
value: ast.expr # noqa: WPS110
import ast
import types
from typing import Final
from typing_extensions import Final
#: That's how python types and ast types map to each other, copied from ast.
_CONST_NODE_TYPE_NAMES: Final = types.MappingProxyType({
bool: 'NameConstant', # should be before int
type(None): 'NameConstant',
int: 'Num',
float: 'Num',
complex: 'Num',
str: 'Str',
bytes: 'Bytes',
type(...): 'Ellipsis',
})
_CONST_NODE_TYPE_NAMES: Final = types.MappingProxyType(
{
bool: 'NameConstant', # should be before int
type(None): 'NameConstant',
int: 'Num',
float: 'Num',
complex: 'Num',
str: 'Str',
bytes: 'Bytes',
type(...): 'Ellipsis',
},
)

@@ -34,4 +35,4 @@

self,
'visit_{0}'.format(type_name),
f'visit_{type_name}',
self.generic_visit,
)(node)
import ast
from typing import Union
from typing import TypeAlias
from typing_extensions import TypeAlias
from wemake_python_styleguide.compat.nodes import MatchAs, MatchStar, TryStar
from wemake_python_styleguide.compat.nodes import TryStar
from wemake_python_styleguide.compat.nodes import TypeAlias as TypeAliasNode
#: When used with `visit_Try` and visit_TryStar`.
AnyTry: TypeAlias = Union[ast.Try, TryStar]
AnyTry: TypeAlias = ast.Try | TryStar
#: Used when named matches are needed.
NamedMatch: TypeAlias = Union[MatchAs, MatchStar]
NamedMatch: TypeAlias = ast.MatchAs | ast.MatchStar
#: These nodes have `.type_params` on python3.12+:
NodeWithTypeParams: TypeAlias = Union[
ast.ClassDef,
ast.FunctionDef,
ast.AsyncFunctionDef,
TypeAliasNode,
]
NodeWithTypeParams: TypeAlias = (
ast.ClassDef | ast.FunctionDef | ast.AsyncFunctionDef | TypeAliasNode
)

@@ -16,5 +16,4 @@ """

import re
from typing import Final
from typing_extensions import Final
# Internal variables

@@ -49,344 +48,340 @@ # ==================

#: List of functions we forbid to use.
FUNCTIONS_BLACKLIST: Final = frozenset((
# Code generation:
'eval',
'exec',
'compile',
FUNCTIONS_BLACKLIST: Final = frozenset(
(
# Code generation:
'eval',
'exec',
'compile',
# Termination:
'exit',
'quit',
# Magic:
'globals',
'locals',
'vars',
'dir',
# IO:
'print',
'pprint',
'pprint.pprint',
'input',
'breakpoint',
# Attribute access:
'delattr',
# Gratis:
'copyright',
'help',
'credits',
# Dynamic imports:
'__import__',
# OOP:
'staticmethod',
# Mypy:
'reveal_type',
'reveal_locals',
),
)
# Termination:
'exit',
'quit',
# Magic:
'globals',
'locals',
'vars',
'dir',
# IO:
'print',
'pprint',
'pprint.pprint',
'input',
'breakpoint',
# Attribute access:
'hasattr',
'delattr',
# Gratis:
'copyright',
'help',
'credits',
# Dynamic imports:
'__import__',
# OOP:
'staticmethod',
# Mypy:
'reveal_type',
'reveal_locals',
))
#: List of module metadata we forbid to use.
MODULE_METADATA_VARIABLES_BLACKLIST: Final = frozenset((
'__author__',
'__all__',
'__version__',
'__about__',
))
MODULE_METADATA_VARIABLES_BLACKLIST: Final = frozenset(
(
'__author__',
'__all__',
'__version__',
'__about__',
'__copyright__',
),
)
#: List of variable names we forbid to use.
VARIABLE_NAMES_BLACKLIST: Final = frozenset((
# Meaningless words:
'data',
'result',
'results',
'item',
'items',
'value',
'values',
'val',
'vals',
'var',
'vars',
'variable',
'content',
'contents',
'info',
'handle',
'handler',
'file',
'obj',
'objects',
'objs',
'some',
'do',
'param',
'params',
'parameters',
VARIABLE_NAMES_BLACKLIST: Final = frozenset(
(
# Meaningless words:
'data',
'result',
'results',
'item',
'items',
'value',
'values',
'val',
'vals',
'var',
'vars',
'variable',
'content',
'contents',
'info',
'handle',
'handler',
'file',
'obj',
'objects',
'objs',
'some',
'do',
'param',
'params',
'parameters',
# Confusables:
'no',
'true',
'false',
# Names from examples:
'foo',
'bar',
'baz',
),
)
# Confusables:
'no',
'true',
'false',
# Names from examples:
'foo',
'bar',
'baz',
))
#: List of character sequences that are hard to read.
UNREADABLE_CHARACTER_COMBINATIONS: Final = frozenset((
'1l',
'1I',
'0O',
'O0',
# Not included: 'lI', 'l1', 'Il'
# Because these names are quite common in real words.
))
UNREADABLE_CHARACTER_COMBINATIONS: Final = frozenset(
(
'1l',
'1I',
'0O',
'O0',
# Not included: 'lI', 'l1', 'Il'
# Because these names are quite common in real words.
),
)
#: List of special names that are used only as first argument in methods.
SPECIAL_ARGUMENT_NAMES_WHITELIST: Final = frozenset((
'self',
'cls',
'mcs',
))
SPECIAL_ARGUMENT_NAMES_WHITELIST: Final = frozenset(
(
'self',
'cls',
'mcs',
),
)
#: List of all magic methods from the python docs.
ALL_MAGIC_METHODS: Final = frozenset((
'__new__',
'__init__',
'__del__',
ALL_MAGIC_METHODS: Final = frozenset(
(
'__new__',
'__init__',
'__del__',
'__repr__',
'__str__',
'__bytes__',
'__format__',
'__lt__',
'__le__',
'__eq__',
'__ne__',
'__gt__',
'__ge__',
'__hash__',
'__bool__',
'__getattr__',
'__getattribute__',
'__setattr__',
'__delattr__',
'__dir__',
'__get__',
'__set__',
'__delete__',
'__set_name__',
'__init_subclass__',
'__instancecheck__',
'__subclasscheck__',
'__mro_entries__',
'__class_getitem__',
'__call__',
'__len__',
'__length_hint__',
'__getitem__',
'__setitem__',
'__delitem__',
'__missing__',
'__iter__',
'__next__',
'__reversed__',
'__contains__',
'__add__',
'__sub__',
'__mul__',
'__matmul__',
'__truediv__',
'__floordiv__',
'__mod__',
'__divmod__',
'__pow__',
'__lshift__',
'__rshift__',
'__and__',
'__xor__',
'__or__',
'__radd__',
'__rsub__',
'__rmul__',
'__rmatmul__',
'__rtruediv__',
'__rfloordiv__',
'__rmod__',
'__rdivmod__',
'__rpow__',
'__rlshift__',
'__rrshift__',
'__rand__',
'__rxor__',
'__ror__',
'__iadd__',
'__isub__',
'__imul__',
'__imatmul__',
'__itruediv__',
'__ifloordiv__',
'__imod__',
'__ipow__',
'__ilshift__',
'__irshift__',
'__iand__',
'__ixor__',
'__ior__',
'__neg__',
'__pos__',
'__abs__',
'__invert__',
'__complex__',
'__int__',
'__float__',
'__index__',
'__round__',
'__trunc__',
'__floor__',
'__ceil__',
'__oct__',
'__hex__',
'__enter__',
'__exit__',
'__await__',
'__aiter__',
'__anext__',
'__aenter__',
'__aexit__',
# pickling
'__getnewargs_ex__',
'__getnewargs__',
'__getstate__',
'__setstate__',
'__reduce__',
'__reduce_ex__',
'__getinitargs__',
# Python 2
'__long__',
'__coerce__',
'__nonzero__',
'__unicode__',
'__cmp__',
# copy
'__copy__',
'__deepcopy__',
'__replace__',
# typing
'__annotate__',
# dataclasses
'__post_init__',
# attrs:
'__attrs_pre_init__',
'__attrs_init__',
'__attrs_post_init__',
# inspect
'__signature__',
# os.path
'__fspath__',
# sys
'__sizeof__',
),
)
'__repr__',
'__str__',
'__bytes__',
'__format__',
'__lt__',
'__le__',
'__eq__',
'__ne__',
'__gt__',
'__ge__',
'__hash__',
'__bool__',
'__getattr__',
'__getattribute__',
'__setattr__',
'__delattr__',
'__dir__',
'__get__',
'__set__',
'__delete__',
'__set_name__',
'__init_subclass__',
'__instancecheck__',
'__subclasscheck__',
'__class_getitem__',
'__call__',
'__len__',
'__length_hint__',
'__getitem__',
'__setitem__',
'__delitem__',
'__missing__',
'__iter__',
'__next__',
'__reversed__',
'__contains__',
'__add__',
'__sub__',
'__mul__',
'__matmul__',
'__truediv__',
'__floordiv__',
'__mod__',
'__divmod__',
'__pow__',
'__lshift__',
'__rshift__',
'__and__',
'__xor__',
'__or__',
'__radd__',
'__rsub__',
'__rmul__',
'__rmatmul__',
'__rtruediv__',
'__rfloordiv__',
'__rmod__',
'__rdivmod__',
'__rpow__',
'__rlshift__',
'__rrshift__',
'__rand__',
'__rxor__',
'__ror__',
'__iadd__',
'__isub__',
'__imul__',
'__imatmul__',
'__itruediv__',
'__ifloordiv__',
'__imod__',
'__ipow__',
'__ilshift__',
'__irshift__',
'__iand__',
'__ixor__',
'__ior__',
'__neg__',
'__pos__',
'__abs__',
'__invert__',
'__complex__',
'__int__',
'__float__',
'__index__',
'__round__',
'__trunc__',
'__floor__',
'__ceil__',
'__oct__',
'__hex__',
'__enter__',
'__exit__',
'__await__',
'__aiter__',
'__anext__',
'__aenter__',
'__aexit__',
# pickling
'__getnewargs_ex__',
'__getnewargs__',
'__getstate__',
'__setstate__',
'__reduce__',
'__reduce_ex__',
'__getinitargs__',
# Python 2
'__long__',
'__coerce__',
'__nonzero__',
'__unicode__',
'__cmp__',
# copy
'__copy__',
'__deepcopy__',
# dataclasses
'__post_init__',
# attrs:
'__attrs_pre_init__',
'__attrs_init__',
'__attrs_post_init__',
# inspect
'__signature__',
# os.path
'__fspath__',
# sys
'__sizeof__',
))
#: List of magic methods that are forbidden to use.
MAGIC_METHODS_BLACKLIST: Final = frozenset((
# Since we don't use `del`:
'__del__',
'__delitem__',
'__delete__',
MAGIC_METHODS_BLACKLIST: Final = frozenset(
(
# Since we don't use `del`:
'__del__',
'__delitem__',
'__delete__',
# Since we don't use `pickle`:
'__reduce__',
'__reduce_ex__',
'__dir__', # since we don't use `dir()`
'__delattr__', # since we don't use `delattr()`
),
)
# Since we don't use `pickle`:
'__reduce__',
'__reduce_ex__',
'__dir__', # since we don't use `dir()`
'__delattr__', # since we don't use `delattr()`
))
#: List of magic methods that are not allowed to be generators.
YIELD_MAGIC_METHODS_BLACKLIST: Final = ALL_MAGIC_METHODS.difference({
# Allowed to be used with ``yield`` keyword:
'__call__',
'__iter__',
'__aiter__',
})
YIELD_MAGIC_METHODS_BLACKLIST: Final = ALL_MAGIC_METHODS.difference(
{
# Allowed to be used with ``yield`` keyword:
'__call__',
'__iter__',
'__aiter__',
},
)
#: List of magic methods that are not allowed to be async.
ASYNC_MAGIC_METHODS_BLACKLIST: Final = ALL_MAGIC_METHODS.difference({
# In order of appearance on
# https://docs.python.org/3/reference/datamodel.html#basic-customization
# Allowed async magic methods are:
'__anext__',
'__aenter__',
'__aexit__',
'__aiter__',
'__call__',
})
ASYNC_MAGIC_METHODS_BLACKLIST: Final = ALL_MAGIC_METHODS.difference(
{
# In order of appearance on
# https://docs.python.org/3/reference/datamodel.html#basic-customization
# Allowed async magic methods are:
'__anext__',
'__aenter__',
'__aexit__',
'__aiter__',
'__call__',
},
)
#: List of builtin classes that are allowed to subclass.
ALLOWED_BUILTIN_CLASSES: Final = frozenset((
'type',
'object',
))
ALLOWED_BUILTIN_CLASSES: Final = frozenset(
(
'type',
'object',
),
)
#: List of builtins that we allow to shadow.
BUILTINS_WHITELIST: Final = frozenset((
UNUSED_PLACEHOLDER,
'license',
'copyright',
'credits',
))
#: List of nested functions' names we allow to use.
NESTED_FUNCTIONS_WHITELIST: Final = frozenset((
'decorator',
'factory',
'wrapper',
))
NESTED_FUNCTIONS_WHITELIST: Final = frozenset(
(
'decorator',
'factory',
'wrapper',
),
)
#: List of allowed ``__future__`` imports.
FUTURE_IMPORTS_WHITELIST: Final = frozenset((
'annotations',
'generator_stop',
))
FUTURE_IMPORTS_WHITELIST: Final = frozenset(
(
'annotations',
'generator_stop',
),
)
#: List of blacklisted module names.
MODULE_NAMES_BLACKLIST: Final = frozenset((
'util',
'utils',
'utilities',
'helpers',
))
MODULE_NAMES_BLACKLIST: Final = frozenset(
(
'util',
'utils',
'utilities',
'helpers',
),
)
#: List of allowed module magic names.
MAGIC_MODULE_NAMES_WHITELIST: Final = frozenset((
'__init__',
'__main__',
))
MAGIC_MODULE_NAMES_WHITELIST: Final = frozenset(
(
'__init__',
'__main__',
),
)
#: List of bad magic module functions.
MAGIC_MODULE_NAMES_BLACKLIST: Final = frozenset((
'__getattr__',
'__dir__',
))
MAGIC_MODULE_NAMES_BLACKLIST: Final = frozenset(
(
'__getattr__',
'__dir__',
),
)

@@ -397,16 +392,17 @@ #: Regex pattern to name modules.

#: Common numbers that are allowed to be used without being called "magic".
MAGIC_NUMBERS_WHITELIST: Final = frozenset((
0, # both int and float
0.1,
0.5,
1.0,
100,
1000,
1024, # bytes
24, # hours
60, # seconds, minutes
MAGIC_NUMBERS_WHITELIST: Final = frozenset(
(
0, # both int and float
0.1,
0.5,
1.0,
100,
1000,
1024, # bytes
24, # hours
60, # seconds, minutes
1j, # imaginary part of a complex number
),
)
1j, # imaginary part of a complex number
))
#: Maximum amount of ``pragma`` no-cover comments per module.

@@ -431,54 +427,43 @@ MAX_NO_COVER_COMMENTS: Final = 5

#: Approximate constants which real values should be imported from math module.
MATH_APPROXIMATE_CONSTANTS: Final = frozenset((
math.pi,
math.e,
math.tau,
))
MATH_APPROXIMATE_CONSTANTS: Final = frozenset(
(
math.pi,
math.e,
math.tau,
),
)
#: List of vague method names that may cause confusion if imported as is:
VAGUE_IMPORTS_BLACKLIST: Final = frozenset((
'read',
'write',
'load',
'loads',
'dump',
'dumps',
'parse',
'safe_load',
'safe_dump',
'load_all',
'dump_all',
'safe_load_all',
'safe_dump_all',
))
VAGUE_IMPORTS_BLACKLIST: Final = frozenset(
(
'read',
'write',
'load',
'loads',
'dump',
'dumps',
'parse',
'safe_load',
'safe_dump',
'load_all',
'dump_all',
'safe_load_all',
'safe_dump_all',
),
)
#: List of literals without arguments we forbid to use.
LITERALS_BLACKLIST: Final = frozenset((
'int',
'float',
'str',
'bytes',
'bool',
'complex',
))
#: List of functions in which arguments must be tuples.
TUPLE_ARGUMENTS_METHODS: Final = frozenset((
'frozenset',
))
TUPLE_ARGUMENTS_METHODS: Final = frozenset(('frozenset',))
#: Conditions that can appear in the ``if`` statement to allow nested imports.
ALLOWED_NESTED_IMPORTS_CONDITIONS: Final = frozenset((
'TYPE_CHECKING',
))
#: List of commonly used aliases
ALIAS_NAMES_WHITELIST: Final = frozenset((
'np',
'pd',
'df',
'plt',
'sns',
'tf',
'cv',
))
ALIAS_NAMES_WHITELIST: Final = frozenset(
(
'np',
'pd',
'df',
'plt',
'sns',
'tf',
'cv',
),
)

@@ -28,9 +28,9 @@ """

import os
from collections import defaultdict
from os import environ
from typing import ClassVar, DefaultDict, Final, List
from typing import ClassVar, Final
from flake8.formatting.base import BaseFormatter
from flake8.statistics import Statistics
from flake8.style_guide import Violation
from flake8.statistics import Statistic, Statistics
from flake8.violation import Violation
from pygments import highlight

@@ -52,8 +52,8 @@ from pygments.formatters import TerminalFormatter

#: See https://no-color.org
_NO_COLOR: Final = environ.get('NO_COLOR', '0') == '1'
_NO_COLOR: Final = os.environ.get('NO_COLOR', '0') == '1'
class WemakeFormatter(BaseFormatter): # type: ignore[misc] # noqa: WPS214
class WemakeFormatter(BaseFormatter): # noqa: WPS214
"""
We need to format our style :term:`violations <violation>` beatifully.
We need to format our style :term:`violations <violation>` beautifully.

@@ -81,3 +81,3 @@ The default formatter does not allow us to do that.

# Logic:
self._processed_filenames: List[str] = []
self._processed_filenames: list[str] = []
self._error_count = 0

@@ -109,3 +109,3 @@

text=error.text,
row_col='{0}:{1}'.format(error.line_number, error.column_number),
row_col=f'{error.line_number}:{error.column_number}',
)

@@ -115,3 +115,3 @@

"""Called when ``--show-source`` option is provided."""
if not self._should_show_source(error):
if not self._should_show_source(error) or not error.physical_line:
return ''

@@ -153,3 +153,3 @@

self._write(self.newline)
self._write(_underline(_bold('All errors: {0}'.format(all_errors))))
self._write(_underline(_bold(f'All errors: {all_errors}')))

@@ -175,31 +175,21 @@ def stop(self) -> None:

def _print_header(self, filename: str) -> None:
self._write(
'{newline}{filename}'.format(
filename=_underline(_bold(filename)),
newline=self.newline,
),
)
header = _underline(_bold(os.path.normpath(filename)))
self._write(f'{self.newline}{header}')
def _print_violation_per_file(
self,
statistic: Statistics,
statistic: Statistic,
error_code: str,
count: int,
error_by_file: DefaultDict[str, int],
error_by_file: defaultdict[str, int],
) -> None:
bold_code = _bold(error_code)
self._write(
'{newline}{error_code}: {message}'.format(
newline=self.newline,
error_code=_bold(error_code),
message=statistic.message,
),
f'{self.newline}{bold_code}: {statistic.message}',
)
for filename, error_count in error_by_file.items():
self._write(
' {error_count:<5} {filename}'.format(
error_count=error_count,
filename=filename,
),
f' {error_count:<5} {filename}',
)
self._write(_underline('Total: {0}'.format(count)))
self._write(_underline(f'Total: {count}'))

@@ -212,2 +202,3 @@ def _should_show_source(self, error: Violation) -> bool:

def _bold(text: str, *, no_color: bool = _NO_COLOR) -> str:

@@ -228,3 +219,3 @@ r"""

return text
return '\033[1m{0}\033[0m'.format(text)
return f'\033[1m{text}\033[0m'

@@ -247,3 +238,3 @@

return text
return '\033[4m{0}\033[0m'.format(text)
return f'\033[4m{text}\033[0m'

@@ -272,3 +263,5 @@

return highlight( # type: ignore[no-any-return]
source, lexer, formatter,
source,
lexer,
formatter,
)

@@ -283,7 +276,8 @@ except Exception: # pragma: no cover

def _count_per_filename(
statistics: Statistics,
error_code: str,
) -> DefaultDict[str, int]:
filenames: DefaultDict[str, int] = defaultdict(int)
) -> defaultdict[str, int]:
filenames: defaultdict[str, int] = defaultdict(int)
stats_for_error_code = statistics.statistics_for(error_code)

@@ -290,0 +284,0 @@

import ast
from typing import Iterator, Sequence
from collections.abc import Iterator, Sequence

@@ -4,0 +4,0 @@

import ast
from collections.abc import Mapping
from itertools import zip_longest
from typing import List, Mapping, Optional, Tuple
from wemake_python_styleguide import constants, types
from wemake_python_styleguide import types
from wemake_python_styleguide.logic.arguments.call_args import get_starred_args
from wemake_python_styleguide.logic.arguments.special_args import (
clean_special_argument,
)

@@ -21,12 +24,8 @@

def _get_args_without_special_argument(
def get_args_without_special_argument(
node: types.AnyFunctionDefAndLambda,
) -> List[ast.arg]:
) -> list[ast.arg]:
"""Gets ``node`` arguments excluding ``self``, ``cls``, ``mcs``."""
node_args = node.args.posonlyargs + node.args.args
if not node_args or isinstance(node, ast.Lambda):
return node_args
if node_args[0].arg not in constants.SPECIAL_ARGUMENT_NAMES_WHITELIST:
return node_args
return node_args[1:]
return clean_special_argument(node, node_args)

@@ -39,3 +38,3 @@

"""Tells whether ``call`` has the same vararg ``*args`` as ``node``."""
vararg_name: Optional[str] = None
vararg_name: str | None = None
for starred_arg in get_starred_args(call):

@@ -57,7 +56,7 @@ # 'args': [<_ast.Starred object at 0x10d77a3c8>]

"""Tells whether ``call`` has the same kwargs as ``node``."""
kwarg_name: Optional[str] = None
kwarg_name: str | None = None
null_arg_keywords = filter(lambda key: key.arg is None, call.keywords)
for keyword in null_arg_keywords:
# `a=1` vs `**kwargs`:
# {'arg': 'a', 'value': <_ast.Num object at 0x1027882b0>}
# {'arg': 'a', 'value': <_ast.Constant(1) object at 0x1027882b0>}
# {'arg': None, 'value': <_ast.Name object at 0x102788320>}

@@ -78,3 +77,3 @@ if isinstance(keyword.value, ast.Name):

"""Tells whether ``call`` has the same positional args as ``node``."""
node_args = _get_args_without_special_argument(node)
node_args = get_args_without_special_argument(node)
paired_arguments = zip_longest(call.args, node_args)

@@ -99,3 +98,3 @@ for call_arg, func_arg in paired_arguments:

call: ast.Call,
) -> Tuple[Mapping[str, ast.keyword], List[ast.keyword]]:
) -> tuple[Mapping[str, ast.keyword], list[ast.keyword]]:
prepared_kw_args = {}

@@ -102,0 +101,0 @@ real_kw_args = []

import ast
from typing import Dict, Optional

@@ -38,3 +37,3 @@

*names: str,
) -> Dict[str, ast.expr]:
) -> dict[str, ast.expr]:
"""Returns keywords of ``call`` by specified ``names``."""

@@ -51,4 +50,4 @@ return {

if len(call.args) == 2: # branch for super(Test, self)
arg1: Optional[ast.expr] = call.args[0]
arg2: Optional[ast.expr] = call.args[1]
arg1: ast.expr | None = call.args[0]
arg2: ast.expr | None = call.args[1]
elif len(call.keywords) == 2: # branch for super(t=Test, obj=self)

@@ -66,3 +65,3 @@ keyword_args = _get_keyword_args_by_names(call, 't', 'obj')

def _get_super_call(node: ast.AST) -> Optional[ast.Call]:
def _get_super_call(node: ast.AST) -> ast.Call | None:
"""Returns given ``node`` if it represents ``super`` ``ast.Call``."""

@@ -69,0 +68,0 @@ if not isinstance(node, ast.Call):

@@ -11,12 +11,7 @@ """

import ast
from typing import Union
from typing import TypeAlias
from typing_extensions import TypeAlias
_Annotation: TypeAlias = ast.expr | ast.Constant
_Annotation: TypeAlias = Union[
ast.expr,
ast.Str,
]
def get_annotation_complexity(annotation_node: _Annotation) -> int:

@@ -29,8 +24,15 @@ """

"""
if isinstance(annotation_node, ast.Str):
if isinstance(annotation_node, ast.Constant) and isinstance(
annotation_node.value,
str,
):
# try to parse string-wrapped annotations
try:
annotation_node = ast.parse( # type: ignore
annotation_node.s,
).body[0].value
annotation_node = (
ast.parse( # type: ignore
annotation_node.value,
)
.body[0]
.value
)
except Exception:

@@ -41,3 +43,3 @@ return 1

return 1 + get_annotation_complexity(annotation_node.slice)
elif isinstance(annotation_node, (ast.Tuple, ast.List)):
if isinstance(annotation_node, ast.Tuple | ast.List):
return max(

@@ -44,0 +46,0 @@ (get_annotation_complexity(node) for node in annotation_node.elts),

@@ -19,3 +19,3 @@ """

import ast
from typing import Callable, Tuple
from collections.abc import Callable

@@ -32,2 +32,3 @@ from wemake_python_styleguide.logic.tree import bools, recursion

ast.IfExp,
ast.match_case,
)

@@ -74,12 +75,12 @@

increment_by: int,
) -> Tuple[int, int, bool]:
) -> tuple[int, int, bool]:
if isinstance(node, _SHORT_CIRCUITS):
return increment_by, max(1, increment_by), True
elif isinstance(node, _CONTROL_FLOW_BREAKERS):
if isinstance(node, _CONTROL_FLOW_BREAKERS):
increment_by += 1
return increment_by, max(1, increment_by), True
elif isinstance(node, _INCREMENTERS):
if isinstance(node, _INCREMENTERS):
increment_by += 1
return increment_by, 0, True
elif isinstance(node, ast.BoolOp):
if isinstance(node, ast.BoolOp):
inner_boolops_amount = bools.count_boolops(node)

@@ -119,4 +120,3 @@ base_complexity = inner_boolops_amount * max(increment_by, 1)

complexity = sum(
_get_cognitive_complexity_for_node(node)
for node in funcdef.body
_get_cognitive_complexity_for_node(node) for node in funcdef.body
)

@@ -123,0 +123,0 @@ if recursion.has_recursive_calls(funcdef):

from collections import defaultdict
from typing import DefaultDict, List
from typing import TypeAlias, final
import attr
from typing_extensions import TypeAlias, final

@@ -13,9 +12,9 @@ from wemake_python_styleguide.types import (

#: Function complexity counter.
FunctionCounter: TypeAlias = DefaultDict[AnyFunctionDef, int]
FunctionCounter: TypeAlias = defaultdict[AnyFunctionDef, int]
#: Function and lambda complexity counter.
FunctionCounterWithLambda: TypeAlias = DefaultDict[AnyFunctionDefAndLambda, int]
FunctionCounterWithLambda: TypeAlias = defaultdict[AnyFunctionDefAndLambda, int]
#: Function and their variables.
FunctionNames: TypeAlias = DefaultDict[AnyFunctionDef, List[str]]
FunctionNames: TypeAlias = defaultdict[AnyFunctionDef, list[str]]

@@ -22,0 +21,0 @@

import ast
from typing import Union
from wemake_python_styleguide.compat.aliases import FunctionNodes
from wemake_python_styleguide.constants import SPECIAL_ARGUMENT_NAMES_WHITELIST
from wemake_python_styleguide.constants import (
SPECIAL_ARGUMENT_NAMES_WHITELIST,
)
from wemake_python_styleguide.logic import nodes, walk

@@ -37,3 +38,3 @@ from wemake_python_styleguide.logic.arguments import call_args

We use this predicates because decorators can be used miltiple times.
We use this predicates because decorators can be used multiple times.
Like ``@auth_required(login_url=LOGIN_URL)`` and similar.

@@ -60,3 +61,3 @@ """

"""
self_node: Union[ast.Attribute, ast.Subscript, None] = None
self_node: ast.Attribute | ast.Subscript | None = None
if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute):

@@ -68,5 +69,5 @@ self_node = node.func

return bool(
self_node and
isinstance(self_node.value, ast.Name) and
self_node.value.id in SPECIAL_ARGUMENT_NAMES_WHITELIST,
self_node
and isinstance(self_node.value, ast.Name)
and self_node.value.id in SPECIAL_ARGUMENT_NAMES_WHITELIST,
)

@@ -86,12 +87,13 @@

"""
if isinstance(node, (ast.Tuple, ast.List)):
if isinstance(node, ast.Tuple | ast.List):
return not node.elts # we do allow `[]` and `()`
elif isinstance(node, ast.Set):
return ( # we do allow `{*set_items}`
len(node.elts) == 1 and
isinstance(node.elts[0], ast.Starred)
if isinstance(node, ast.Set):
elts = node.elts
return len(elts) == 1 and isinstance( # we do allow `{*set_items}`
elts[0],
ast.Starred,
)
elif isinstance(node, ast.Dict): # we do allow `{}` and `{**values}`
if isinstance(node, ast.Dict): # we do allow `{}` and `{**values}`
return not list(filter(None, node.keys))
elif isinstance(node, ast.Call):
if isinstance(node, ast.Call):
return not call_args.get_all_args(node) # we do allow `call()`

@@ -113,5 +115,7 @@ return False

# We allow variables, attributes, subscripts, and `-1`
if isinstance(node.operand, (ast.Constant, ast.Num)):
return node.operand.n == 1
if isinstance(node.operand, ast.Constant) and isinstance(
node.operand.value, int
):
return node.operand.value == 1
return True
return False
import re
from typing import Final
from typing_extensions import Final
#: Used as a special name patterns for unused variables, like `_` and `__`.
_UNUSED_VARIABLE_REGEX: Final = re.compile('^_+$')
_UNUSED_VARIABLE_REGEX: Final = re.compile(r'^_+$')

@@ -156,6 +155,6 @@

return (
not is_protected(name) and
not is_private(name) and
not is_magic(name) and
not is_unused(name)
not is_protected(name)
and not is_private(name)
and not is_magic(name)
and not is_unused(name)
)
import re
from typing import Iterable
from collections.abc import Iterable
from typing import Final
from typing_extensions import Final
from wemake_python_styleguide.logic.naming import access

@@ -91,9 +90,2 @@

False
>>> does_contain_unicode('привет_мир1')
True
>>> does_contain_unicode('russian_техт')
True
"""

@@ -100,0 +92,0 @@ try:

@@ -1,12 +0,14 @@

from functools import lru_cache
from typing import FrozenSet
from functools import cache
from wemake_python_styleguide.constants import VARIABLE_NAMES_BLACKLIST
from wemake_python_styleguide.types import ConfigurationOptions
from wemake_python_styleguide.constants import (
MODULE_METADATA_VARIABLES_BLACKLIST,
VARIABLE_NAMES_BLACKLIST,
)
from wemake_python_styleguide.options.validation import ValidatedOptions
@lru_cache()
@cache
def variable_names_blacklist_from(
options: ConfigurationOptions,
) -> FrozenSet[str]:
options: ValidatedOptions,
) -> frozenset[str]:
"""Creates variable names blacklist from options and constants."""

@@ -20,1 +22,15 @@ variable_names_blacklist = {

)
@cache
def module_metadata_blacklist(
options: ValidatedOptions,
) -> frozenset[str]:
"""Creates module metadata blacklist from options and constants."""
module_metadata_blacklist = {
*MODULE_METADATA_VARIABLES_BLACKLIST,
*options.forbidden_module_metadata,
}
return frozenset(
module_metadata_blacklist - set(options.allowed_module_metadata),
)
import builtins
import inspect
import keyword
from typing import Final
from typing_extensions import Final
from wemake_python_styleguide.constants import BUILTINS_WHITELIST
from wemake_python_styleguide.logic.naming.access import is_magic, is_unused
_BUILTINS: Final = frozenset((
builtin[0]
for builtin in inspect.getmembers(builtins)
if builtin[0] not in BUILTINS_WHITELIST
))
_BUILTINS: Final = frozenset(
builtin[0] for builtin in inspect.getmembers(builtins)
)
_ALL_BUILTINS: Final = frozenset((
*keyword.kwlist,
*_BUILTINS,
_ALL_BUILTINS: Final = frozenset(
(
*keyword.kwlist,
*_BUILTINS,
# Special case.
# Some python version have them, some do not have them:
'async',
'await',
),
)
# Special case.
# Some python version have them, some do not have them:
'async',
'await',
))
def is_builtin_name(variable_name: str) -> bool:

@@ -28,0 +25,0 @@ """

@@ -1,2 +0,2 @@

from typing import Iterable
from collections.abc import Iterable

@@ -36,4 +36,4 @@ from wemake_python_styleguide.constants import (

name_to_check,
'_{0}'.format(name_to_check),
'{0}_'.format(name_to_check),
f'_{name_to_check}',
f'{name_to_check}_',
}

@@ -40,0 +40,0 @@ if name.lower() in choices_to_check:

import ast
import itertools
from typing import Iterable, List, Optional
from collections.abc import Iterable
from wemake_python_styleguide.compat.functions import get_assign_targets
from wemake_python_styleguide.types import AnyAssignWithWalrus
from wemake_python_styleguide.types import AnyAssignWithWalrus, AnyNodes

@@ -16,3 +16,3 @@

def get_assigned_name(node: ast.AST) -> Optional[str]:
def get_assigned_name(node: ast.AST) -> str | None:
"""

@@ -56,10 +56,14 @@ Returns variable names for node that is just assigned.

"""
return itertools.chain.from_iterable((
return itertools.chain.from_iterable(
get_variables_from_node(target)
for node in nodes
for target in get_assign_targets(node)
))
)
def get_variables_from_node(node: ast.AST) -> List[str]:
def get_variables_from_node(
node: ast.AST,
*,
exclude: AnyNodes = (),
) -> list[str]:
"""

@@ -74,4 +78,4 @@ Gets the assigned names from the list of nodes.

"""
names: List[str] = []
naive_attempt = extract_name(node)
names: list[str] = []
naive_attempt = extract_name(node, exclude=exclude)

@@ -84,7 +88,7 @@ if naive_attempt:

for subnode in node.elts:
names.extend(get_variables_from_node(subnode))
names.extend(get_variables_from_node(subnode, exclude=exclude))
return names
def extract_name(node: ast.AST) -> Optional[str]:
def extract_name(node: ast.AST, *, exclude: AnyNodes = ()) -> str | None:
"""

@@ -105,3 +109,7 @@ Utility to extract names for several types of nodes.

>>> assert extract_name(node, exclude=(ast.Name,)) is None
"""
if isinstance(node, exclude):
return None
if isinstance(node, ast.Starred):

@@ -108,0 +116,0 @@ return extract_name(node.value)

import ast
from typing import Optional, Union
from wemake_python_styleguide.logic.safe_eval import literal_eval_with_names
from wemake_python_styleguide.types import ContextNodes

@@ -22,3 +20,3 @@

def get_parent(node: ast.AST) -> Optional[ast.AST]:
def get_parent(node: ast.AST) -> ast.AST | None:
"""Returns the parent node or ``None`` if node has no parent."""

@@ -28,16 +26,4 @@ return getattr(node, 'wps_parent', None)

def get_context(node: ast.AST) -> Optional[ContextNodes]:
def get_context(node: ast.AST) -> ContextNodes | None:
"""Returns the context or ``None`` if node has no context."""
return getattr(node, 'wps_context', None)
def evaluate_node(node: ast.AST) -> Union[int, float, str, bytes, None]:
"""Returns the value of a node or its evaluation."""
if isinstance(node, ast.Name):
return None
if isinstance(node, (ast.Str, ast.Bytes)):
return node.s
try:
return literal_eval_with_names(node) # type: ignore[no-any-return]
except Exception:
return None
import ast
import astor
from wemake_python_styleguide.types import AnyTextPrimitive

@@ -10,3 +8,3 @@

"""Returns the source code by doing ``ast`` to string convert."""
return astor.to_source(node).strip() # type: ignore[no-any-return]
return ast.unparse(node).strip()

@@ -13,0 +11,0 @@

import tokenize
import types
from typing import Final
from typing_extensions import Final
#: Pairs of matching bracket types.
MATCHING_BRACKETS: Final = types.MappingProxyType({
tokenize.LBRACE: tokenize.RBRACE,
tokenize.LSQB: tokenize.RSQB,
tokenize.LPAR: tokenize.RPAR,
})
#: Constant for several types of new lines in Python's grammar.
NEWLINES: Final = frozenset((
tokenize.NL,
tokenize.NEWLINE,
))
#: We do allow some tokens on empty lines.
ALLOWED_EMPTY_LINE_TOKENS: Final = frozenset((
*NEWLINES,
*MATCHING_BRACKETS.values(),
))
NEWLINES: Final = frozenset(
(
tokenize.NL,
tokenize.NEWLINE,
),
)
import tokenize
from typing import Sequence
from collections.abc import Sequence

@@ -23,4 +23,4 @@ from wemake_python_styleguide.logic.tokens.constants import NEWLINES

tokens[index]
for index in range(token_position + 1, len(tokens)) # noqa: WPS518
for index in range(token_position + 1, len(tokens))
if tokens[index].exact_type not in NEWLINES
)
import tokenize
from typing import Tuple
from typing import Final
from flake8_quotes.docstring_detection import ( # noqa: WPS113, F401
get_docstring_tokens as get_docstring_tokens,
)
#: All tokens that don't really mean anything for user.
_UTILITY_TOKENS: Final = frozenset((
tokenize.NEWLINE,
tokenize.INDENT,
tokenize.DEDENT,
tokenize.NL,
tokenize.COMMENT,
))
def split_prefixes(string: str) -> Tuple[str, str]:
def split_prefixes(string: str) -> tuple[str, str]:
"""

@@ -28,7 +33,10 @@ Splits string repr by prefixes and the quoted content.

"""Tells whether string token is written as inside triple quotes."""
if string_contents.startswith('"""') and string_contents.endswith('"""'):
return True
elif string_contents.startswith("'''") and string_contents.endswith("'''"):
return True
return False
_mods, string_contents = split_prefixes(string_contents)
return bool(
(string_contents.startswith('"""') and string_contents.endswith('"""'))
or (
string_contents.startswith("'''")
and string_contents.endswith("'''")
),
)

@@ -39,1 +47,6 @@

return token.string[1:].strip()
def is_meaningful_token(token: tokenize.TokenInfo) -> bool:
"""Returns `True` if some token is a real, not utility token."""
return token.exact_type not in _UTILITY_TOKENS
import ast
from typing import Final
from typing_extensions import Final
from wemake_python_styleguide.compat.aliases import FunctionNodes

@@ -15,3 +14,2 @@ from wemake_python_styleguide.logic import walk

ast.Attribute,
ast.Str,
ast.List,

@@ -28,6 +26,9 @@ ast.Tuple,

We use this predicate to allow all types of repetetive
We use this predicate to allow all types of repetitive
function and instance annotations.
"""
if not isinstance(node, _AnnParts):
if not (
isinstance(node, _AnnParts)
or (isinstance(node, ast.Constant) and isinstance(node.value, str))
):
return False

@@ -38,12 +39,11 @@

contains_node = bool(
annotated.returns and
walk.is_contained_by(node, annotated.returns),
annotated.returns and walk.is_contained_by(node, annotated.returns),
)
return node == annotated.returns or contains_node
elif isinstance(annotated, _AnnNodes):
if isinstance(annotated, _AnnNodes):
contains_node = bool(
annotated.annotation and
walk.is_contained_by(node, annotated.annotation),
annotated.annotation
and walk.is_contained_by(node, annotated.annotation),
)
return node == annotated.annotation or contains_node
return False
import ast
from typing import Iterable, Optional
from collections.abc import Iterable
from wemake_python_styleguide.constants import SPECIAL_ARGUMENT_NAMES_WHITELIST
from wemake_python_styleguide.types import AnyChainable, AnyVariableDef
from wemake_python_styleguide.types import AnyNodes, AnyVariableDef
def _chained_item(iterator: ast.AST) -> Optional[ast.AST]:
if isinstance(iterator, (ast.Attribute, ast.Subscript)):
def _chained_item(iterator: ast.AST) -> ast.AST | None:
if isinstance(iterator, ast.Attribute | ast.Subscript):
return iterator.value
elif isinstance(iterator, ast.Call):
if isinstance(iterator, ast.Call):
return iterator.func

@@ -16,3 +16,3 @@ return None

def parts(node: AnyChainable) -> Iterable[ast.AST]:
def parts(node: ast.AST) -> Iterable[ast.AST]:
"""

@@ -29,3 +29,3 @@ Returns all ``.`` separated elements for attributes, subscripts and calls.

"""
iterator: ast.AST = node
iterator = node

@@ -41,2 +41,7 @@ while True:

def only_consists_of_parts(node: ast.AST, allowed: AnyNodes) -> bool:
"""Returns `True` if some node consists of only given parts."""
return all(isinstance(part, allowed) for part in parts(node))
def is_foreign_attribute(node: AnyVariableDef) -> bool:

@@ -55,1 +60,8 @@ """Tells whether this node is a foreign attribute."""

return node.value.id not in SPECIAL_ARGUMENT_NAMES_WHITELIST
def is_special_attr(node: ast.Attribute) -> bool:
"""Finds attributes that are assigned to `self`, `cls`, etc."""
if not isinstance(node.value, ast.Name):
return False
return node.value.id in SPECIAL_ARGUMENT_NAMES_WHITELIST

@@ -6,6 +6,8 @@ import ast

"""Counts how many ``BoolOp`` nodes there are in a node."""
return len([
subnode
for subnode in ast.walk(node)
if isinstance(subnode, ast.BoolOp)
])
return len(
[
subnode
for subnode in ast.walk(node)
if isinstance(subnode, ast.BoolOp)
],
)
import ast
from typing import Iterable, Optional
from collections.abc import Iterable
def _chained_item(iterator: ast.AST) -> Optional[ast.Call]:
def _chained_item(iterator: ast.AST) -> ast.Call | None:
children = list(ast.iter_child_nodes(iterator))

@@ -7,0 +7,0 @@ if isinstance(children[0], ast.Call):

import ast
from typing import List, Optional, Tuple
from typing import Final, TypeAlias
from wemake_python_styleguide.compat.aliases import AssignNodes
from wemake_python_styleguide.constants import ALLOWED_BUILTIN_CLASSES
from wemake_python_styleguide.logic import nodes
from wemake_python_styleguide.logic import nodes, source
from wemake_python_styleguide.logic.naming.builtins import is_builtin_name

@@ -11,6 +11,28 @@ from wemake_python_styleguide.types import AnyAssign

#: Type alias for the attributes we return from class inspection.
_AllAttributes = Tuple[List[AnyAssign], List[ast.Attribute]]
_AllAttributes: TypeAlias = tuple[list[AnyAssign], list[ast.Attribute]]
#: Names that can define a dataclass.
_DATACLASS_NAMES: Final = frozenset((
# stdlib:
'dataclasses.dataclass',
# attrs:
'attrs.define',
'attrs.frozen',
'attrs.mutable',
'attr.s',
'attr.attrs',
'attr.attributes',
'attr.frozen',
'attr.mutable',
'attr.dataclass',
# pydantic also has `dataclass` and `dataclasses.dataclass`
))
def is_forbidden_super_class(class_name: Optional[str]) -> bool:
#: Short form of dataclass decorators without module names.
_SHORT_DATACLASS_NAMES: Final = frozenset(
dataclass_name.split('.')[1] for dataclass_name in _DATACLASS_NAMES
)
def is_forbidden_super_class(class_name: str | None) -> bool:
"""

@@ -45,2 +67,20 @@ Tells whether or not the base class is forbidden to be subclassed.

def is_dataclass(node: ast.ClassDef) -> bool:
"""Checks if some class is defined as a dataclass using popular libs."""
for decorator in node.decorator_list:
if isinstance(decorator, ast.Call):
decorator = decorator.func # noqa: PLW2901
if not isinstance(decorator, ast.Name | ast.Attribute):
continue
decorator_code = source.node_to_string(decorator)
if (
decorator_code in _DATACLASS_NAMES
or decorator_code in _SHORT_DATACLASS_NAMES
):
return True
return False
def get_attributes(

@@ -71,3 +111,3 @@ node: ast.ClassDef,

for subnode in ast.walk(node):
instance_attr = _get_instance_attribute(subnode)
instance_attr = get_instance_attribute(subnode)
if instance_attr is not None:

@@ -87,9 +127,14 @@ instance_attributes.append(instance_attr)

def _get_instance_attribute(node: ast.AST) -> Optional[ast.Attribute]:
return node if (
isinstance(node, ast.Attribute) and
isinstance(node.ctx, ast.Store) and
isinstance(node.value, ast.Name) and
node.value.id == 'self'
) else None
def get_instance_attribute(node: ast.AST) -> ast.Attribute | None:
"""Returns node if this is an instance attribute or `None` if not."""
return (
node
if (
isinstance(node, ast.Attribute)
and isinstance(node.ctx, ast.Store)
and isinstance(node.value, ast.Name)
and node.value.id == 'self'
)
else None
)

@@ -100,8 +145,12 @@

subnode: ast.AST,
) -> Optional[AnyAssign]:
return subnode if (
nodes.get_context(subnode) is node and
getattr(subnode, 'value', None) and
isinstance(subnode, AssignNodes)
) else None
) -> AnyAssign | None:
return (
subnode
if (
nodes.get_context(subnode) is node
and getattr(subnode, 'value', None)
and isinstance(subnode, AssignNodes)
)
else None
)

@@ -112,9 +161,16 @@

subnode: ast.AST,
) -> Optional[AnyAssign]:
return subnode if (
nodes.get_context(subnode) is node and
(
getattr(subnode, 'value', None) and
isinstance(subnode, AssignNodes)
) or isinstance(subnode, ast.AnnAssign)
) else None
) -> AnyAssign | None:
return (
subnode
if (
(
nodes.get_context(subnode) is node
and (
getattr(subnode, 'value', None)
and isinstance(subnode, AssignNodes)
)
)
or isinstance(subnode, ast.AnnAssign)
)
else None
)
import ast
from collections.abc import Iterable, Sequence
from functools import partial
from typing import ( # noqa: WPS235
Iterable,
List,
Optional,
Sequence,
Tuple,
Type,
TypeVar,
Union,
cast,
)
from typing import TypeVar, cast

@@ -19,31 +10,4 @@ _NodeType = TypeVar('_NodeType')

def normalize_dict_elements(node: ast.Dict) -> Sequence[ast.AST]:
"""
Normalizes ``dict`` elements and enforces consistent order.
We had a problem that some ``dict`` objects might not have some keys.
Example::
some_dict = {**one, **two}
This ``dict`` contains two values and zero keys.
This function will normalize this structure to use
values instead of missing keys.
See also:
https://github.com/wemake-services/wemake-python-styleguide/issues/450
"""
elements: List[ast.AST] = []
for dict_key, dict_value in zip(node.keys, node.values):
if dict_key is None:
elements.append(dict_value)
else:
elements.append(dict_key)
return elements
def sequence_of_node(
node_types: Tuple[Type[_NodeType], ...],
node_types: tuple[type[_NodeType], ...],
sequence: Sequence[ast.stmt],

@@ -53,3 +17,4 @@ ) -> Iterable[Sequence[_NodeType]]:

is_desired_type = partial(
lambda types, node: isinstance(node, types), node_types,
lambda types, node: isinstance(node, types),
node_types,
)

@@ -59,3 +24,3 @@

previous_node = next(sequence_iterator, None)
node_sequence: List[_NodeType] = []
node_sequence: list[_NodeType] = []

@@ -76,5 +41,5 @@ while previous_node is not None:

sequence: Iterable[_NodeType],
default: Optional[_DefaultType] = None,
) -> Union[_NodeType, _DefaultType, None]:
default: _DefaultType | None = None,
) -> _NodeType | _DefaultType | None:
"""Get first variable from sequence or default."""
return next(iter(sequence), default)
import ast
import types
from collections import defaultdict
from typing import DefaultDict, Mapping, Set, Tuple, Type, Union
from collections.abc import Mapping
from typing import Final, TypeAlias
import attr
from typing_extensions import Final, final
from wemake_python_styleguide.logic import source
#: Type to represent multiple simple operators.
_MultipleCompareOperators: TypeAlias = tuple[type[ast.cmpop], ...]
@final
@attr.dataclass(frozen=True, slots=True)
class _Bounds:
"""Represents the bounds we use to calculate the similar compare nodes."""
lower_bound: Set[ast.Compare] = attr.ib(factory=set)
upper_bound: Set[ast.Compare] = attr.ib(factory=set)
_MultipleCompareOperators = Tuple[Type[ast.cmpop], ...]
#: Type to represent `SIMILAR_OPERATORS` constant.
_ComparesMapping = Mapping[
Type[ast.cmpop],
#: Type to represent `_SIMILAR_OPERATORS` constant.
_ComparesMapping: TypeAlias = Mapping[
type[ast.cmpop],
_MultipleCompareOperators,
]
#: Used to track the operator usages in `a > b and b >c` compares.
_OperatorUsages = DefaultDict[str, _Bounds]
#: Constant to define similar operators.
SIMILAR_OPERATORS: Final[_ComparesMapping] = types.MappingProxyType({
ast.Gt: (ast.Gt, ast.GtE),
ast.GtE: (ast.Gt, ast.GtE),
ast.Lt: (ast.Lt, ast.LtE),
ast.LtE: (ast.Lt, ast.LtE),
})
_SIMILAR_OPERATORS: Final[_ComparesMapping] = types.MappingProxyType(
{
ast.Gt: (ast.Gt, ast.GtE),
ast.GtE: (ast.Gt, ast.GtE),
ast.Lt: (ast.Lt, ast.LtE),
ast.LtE: (ast.Lt, ast.LtE),
},
)

@@ -43,75 +30,31 @@

operator: ast.cmpop,
) -> Union[Type[ast.cmpop], _MultipleCompareOperators]:
) -> type[ast.cmpop] | _MultipleCompareOperators:
"""Returns similar operators types for the given operator."""
operator_type = operator.__class__
return SIMILAR_OPERATORS.get(operator_type, operator_type)
operator_type = type(operator)
return _SIMILAR_OPERATORS.get(operator_type, operator_type)
@final
class CompareBounds:
"""
Calculates bounds of expressions like ``a > b and b > c`` in python.
Later we call ``.is_valid()`` method to be sure that we raise
violations for incorrect bounds.
Credit goes to:
https://github.com/PyCQA/pylint/blob/master/pylint/checkers/refactoring.py
"""
def __init__(self, node: ast.BoolOp) -> None:
"""Conctructs the basic data to calculate the bounds."""
self._node = node
self._uses: _OperatorUsages = defaultdict(_Bounds)
def is_valid(self) -> bool:
"""We say that bounds are invalid, when we can refactor them."""
local_uses = self._build_bounds().values()
for bounds in local_uses:
num_shared = len(
bounds.lower_bound.intersection(bounds.upper_bound),
)
num_lower_bounds = len(bounds.lower_bound)
num_upper_bounds = len(bounds.upper_bound)
if num_shared < num_lower_bounds and num_shared < num_upper_bounds:
return False
return True
def _build_bounds(self) -> _OperatorUsages:
for comparison_node in self._node.values:
if isinstance(comparison_node, ast.Compare):
self._find_lower_upper_bounds(comparison_node)
return self._uses
def _find_lower_upper_bounds(
self,
comparison_node: ast.Compare,
) -> None:
left_operand = comparison_node.left
comparators = zip(comparison_node.ops, comparison_node.comparators)
for operator, right_operand in comparators:
for operand in (left_operand, right_operand):
self._mutate(
comparison_node,
operator,
source.node_to_string(operand),
operand is left_operand,
)
left_operand = right_operand
def _mutate(
self,
comparison_node: ast.Compare,
operator: ast.cmpop,
name: str,
is_left: bool,
) -> None:
key_name = None
if isinstance(operator, (ast.Lt, ast.LtE)):
key_name = 'lower_bound' if is_left else 'upper_bound'
elif isinstance(operator, (ast.Gt, ast.GtE)):
key_name = 'upper_bound' if is_left else 'lower_bound'
if key_name:
getattr(self._uses[name], key_name).add(comparison_node)
def is_useless_ternary(
node: ast.IfExp,
cmpop: ast.cmpop,
left: ast.expr,
right: ast.expr,
) -> bool:
"""Checks if the given ternary expression parts are useless."""
if isinstance(cmpop, ast.Is | ast.Eq):
comparators = {
source.node_to_string(left),
source.node_to_string(right),
}
common_elements = {
source.node_to_string(node.body),
source.node_to_string(node.orelse),
}.intersection(comparators)
return len(common_elements) == len(comparators)
if isinstance(cmpop, ast.IsNot | ast.NotEq):
return source.node_to_string(node.body) == source.node_to_string(
left,
) and source.node_to_string(node.orelse) == source.node_to_string(
right,
)
return False

@@ -16,10 +16,9 @@ import ast

is_partial_name = (
isinstance(decorator, ast.Name) and
decorator.id == 'overload'
isinstance(decorator, ast.Name) and decorator.id == 'overload'
)
is_full_name = (
isinstance(decorator, ast.Attribute) and
decorator.attr == 'overload' and
isinstance(decorator.value, ast.Name) and
decorator.value.id == 'typing'
isinstance(decorator, ast.Attribute)
and decorator.attr == 'overload'
and isinstance(decorator.value, ast.Name)
and decorator.value.id == 'typing'
)

@@ -26,0 +25,0 @@ if is_partial_name or is_full_name:

import ast
from collections.abc import Mapping
from inspect import getmro
from typing import Dict, List, Mapping, Optional, Tuple, Type
from typing import TypeAlias
from typing_extensions import TypeAlias
from wemake_python_styleguide.compat.types import AnyTry
from wemake_python_styleguide.logic import source
from wemake_python_styleguide.logic.walk import is_contained
from wemake_python_styleguide.types import AnyNodes
def get_exception_name(node: ast.Raise) -> Optional[str]:
def get_exception_name(node: ast.Raise) -> str | None:
"""Returns the exception name or ``None`` if node has not it."""

@@ -19,5 +16,4 @@ exception = node.exc

exception_func = getattr(exception, 'func', None)
if exception_func:
exception = exception_func
if isinstance(exception, ast.Call):
exception = exception.func

@@ -27,3 +23,3 @@ return getattr(exception, 'id', None)

def get_cause_name(node: ast.Raise) -> Optional[str]:
def get_cause_name(node: ast.Raise) -> str | None:
"""Returns the cause name or ``None`` if node has not it."""

@@ -33,5 +29,5 @@ return getattr(node.cause, 'id', None)

def get_all_exception_names(node: AnyTry) -> List[str]:
def get_all_exception_names(node: AnyTry) -> list[str]:
"""Returns a list of all exceptions names in try blocks."""
exceptions: List[str] = []
exceptions: list[str] = []
for exc_handler in node.handlers:

@@ -43,16 +39,15 @@ # There might be complex things hidden inside an exception type,

elif isinstance(exc_handler.type, ast.Tuple):
exceptions.extend([
source.node_to_string(node)
for node in exc_handler.type.elts
])
exceptions.extend(
[source.node_to_string(node) for node in exc_handler.type.elts],
)
return exceptions
_ExceptionMemo: TypeAlias = Dict[str, Tuple[str, ...]]
_ExceptionMemo: TypeAlias = dict[str, tuple[str, ...]]
def traverse_exception(
cls: Type[BaseException],
builtin_exceptions: Optional[_ExceptionMemo] = None,
) -> Mapping[str, Tuple[str, ...]]:
cls: type[BaseException],
builtin_exceptions: _ExceptionMemo | None = None,
) -> Mapping[str, tuple[str, ...]]:
"""

@@ -76,4 +71,4 @@ Returns a dictionary of built-in exceptions hierarchy.

if (
issubclass(base, BaseException) and
base.__name__ != exc.__name__
issubclass(base, BaseException)
and base.__name__ != exc.__name__
)

@@ -84,25 +79,1 @@ )

return builtin_exceptions
def find_returning_nodes(
node: AnyTry,
bad_returning_nodes: AnyNodes,
) -> Tuple[bool, bool, bool, bool]:
"""Find nodes that return value and are inside try/except/else/finally."""
try_has = any(
is_contained(line, bad_returning_nodes)
for line in node.body
)
except_has = any(
is_contained(except_handler, bad_returning_nodes)
for except_handler in node.handlers
)
else_has = any(
is_contained(line, bad_returning_nodes)
for line in node.orelse
)
finally_has = any(
is_contained(line, bad_returning_nodes)
for line in node.finalbody
)
return try_has, except_has, else_has, finally_has

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

from ast import Call, Return, Yield, YieldFrom, arg, walk
from typing import Container, Iterable, List, Optional, Union
import ast
from collections.abc import Container, Iterable
from typing import Final, TypeAlias
from typing_extensions import Final, TypeAlias
from wemake_python_styleguide.compat.aliases import FunctionNodes
from wemake_python_styleguide.logic import source

@@ -14,7 +14,3 @@ from wemake_python_styleguide.logic.walk import is_contained

#: Expressions that causes control transfer from a routine
_AnyControlTransfers: TypeAlias = Union[
Return,
Yield,
YieldFrom,
]
_AnyControlTransfers: TypeAlias = ast.Return | ast.Yield | ast.YieldFrom

@@ -24,7 +20,7 @@ #: Type annotation for an iterable of control transfer nodes

#: Method types
_METHOD_TYPES: Final = frozenset((
'method',
'classmethod',
'staticmethod',
#: That's what we expect from `@overload` decorator:
_OVERLOAD_EXCEPTIONS: Final = frozenset((
'overload',
'typing.overload',
'typing_extensions.overload',
))

@@ -34,3 +30,3 @@

def given_function_called(
node: Call,
node: ast.Call,
to_check: Container[str],

@@ -54,30 +50,4 @@ *,

def is_method(function_type: Optional[str]) -> bool:
def get_all_arguments(node: AnyFunctionDefAndLambda) -> list[ast.arg]:
"""
Returns whether a given function type belongs to a class.
>>> is_method('function')
False
>>> is_method(None)
False
>>> is_method('method')
True
>>> is_method('classmethod')
True
>>> is_method('staticmethod')
True
>>> is_method('')
False
"""
return function_type in _METHOD_TYPES
def get_all_arguments(node: AnyFunctionDefAndLambda) -> List[arg]:
"""
Returns list of all arguments that exist in a function.

@@ -120,3 +90,5 @@

"""Tells whether a given function is a generator."""
return any(is_contained(body, (Yield, YieldFrom)) for body in node.body)
return any(
is_contained(body, (ast.Yield, ast.YieldFrom)) for body in node.body
)

@@ -126,6 +98,14 @@

"""Yields nodes that cause a control transfer from a function."""
control_transfer_nodes = (Return, Yield, YieldFrom)
for body_item in node.body:
for sub_node in walk(body_item):
if isinstance(sub_node, control_transfer_nodes):
for sub_node in ast.walk(body_item):
if isinstance(sub_node, _AnyControlTransfers):
yield sub_node
def is_overload(node: ast.AST) -> bool:
"""Check that function decorated with `typing.overload`."""
if isinstance(node, FunctionNodes):
for decorator in node.decorator_list:
if source.node_to_string(decorator) in _OVERLOAD_EXCEPTIONS:
return True
return False
import ast
from typing import Iterable
from collections.abc import Iterable
from typing import Final
from typing_extensions import Final
from wemake_python_styleguide.compat.aliases import FunctionNodes

@@ -24,11 +23,10 @@ from wemake_python_styleguide.constants import UNUSED_PLACEHOLDER

"""Returns nodes of paired getter or setter methods."""
stack = {}
stack: dict[str, AnyFunctionDef] = {}
for method in _find_getters_and_setters(node):
method_stripped = method.name[GETTER_LENGTH:]
if method_stripped not in stack:
paired_method = stack.pop(method_stripped, None)
if paired_method is None:
stack[method_stripped] = method
else:
yield method
paired_method = stack.pop(method_stripped)
yield paired_method
yield from (method, paired_method)

@@ -49,6 +47,8 @@

for class_attribute in flat_class_attributes
}.union({
instance.attr.lstrip(UNUSED_PLACEHOLDER)
for instance in instance_attributes
})
}.union(
{
instance.attr.lstrip(UNUSED_PLACEHOLDER)
for instance in instance_attributes
},
)

@@ -64,4 +64,7 @@ for method in _find_getters_and_setters(node):

is_correct_context = nodes.get_context(sub) is node
if isinstance(sub, FunctionNodes) and is_correct_context:
if sub.name[:GETTER_LENGTH] in _GetterSetterPrefixes:
yield sub
if (
isinstance(sub, FunctionNodes)
and is_correct_context
and sub.name[:GETTER_LENGTH] in _GetterSetterPrefixes
):
yield sub
import ast
from typing import Iterable, List, Optional, Union
from collections.abc import Iterable, Sequence
from typing import ClassVar, TypeAlias, final
from typing_extensions import TypeAlias
from wemake_python_styleguide.types import AnyIf, AnyNodes
from wemake_python_styleguide.types import AnyNodes
_IfAndElifASTNode: TypeAlias = ast.If | list[ast.stmt]
_IfAndElifASTNode: TypeAlias = Union[ast.If, List[ast.stmt]]
def is_elif(node: ast.If) -> bool:
"""Tells if this node is a part of an ``if`` chain or just a single one."""
return getattr(node, 'wps_if_chain', False) # noqa: WPS425
def has_elif(node: ast.If) -> bool:
"""
Tells if this statement has an ``elif`` chained or not.
Just for informational purposes, the ``if`` chain is a sequence as follows:
``[ast.If, [ast.stmt, ...]]`` for an ``if: ... else: ...``
``[ast.If, ast.If, [ast.stmt, ...]]`` for an ``if: ... elif: ... else: ...``
And so on.
As you can see, a chain that has a length of more than 2 has ``elif`` in it.
"""
return len(tuple(chain(node))) > 2
def has_else(node: ast.If) -> bool:
"""Tells if this node or ``if`` chain ends with an ``else`` expression."""
last_elem = tuple(chain(node))[-1]
return bool(last_elem)
"""Tells if this node or ``if`` chain ends with an ``else`` statement."""
return bool(tuple(chain(node))[-1])
def root_if(node: ast.If) -> Optional[ast.If]:
"""Returns the previous ``if`` node in the chain if it exists."""
return getattr(node, 'wps_if_chained', None)
def chain(node: ast.If) -> Iterable[_IfAndElifASTNode]:

@@ -44,3 +19,3 @@ """

This function also does go not up in the tree
This function also does not go up in the tree
to find all parent ``if`` nodes. The rest order is preserved.

@@ -69,10 +44,55 @@ The first one to return is the node itself.

def has_nodes(
to_check: AnyNodes,
iterable: Iterable[ast.AST],
) -> bool:
"""Finds the given nodes types in ``if`` body."""
return any(
isinstance(line, to_check)
for line in iterable
@final
class NegatedIfConditions:
"""Finds negated ``if`` nodes."""
_negated_ops: ClassVar[AnyNodes] = (
ast.NotEq,
ast.IsNot,
ast.NotIn,
)
def __init__(self) -> None:
"""Collects visited nodes not to double report them."""
self._visited_ifs: set[ast.If] = set()
def negated_nodes(self, node: AnyIf) -> Sequence[AnyIf]:
"""Returns the list of negated nodes to raise violations on."""
if isinstance(node, ast.If):
return self._process_if(node)
return self._process_ifexpr(node)
def _process_if(self, node: ast.If) -> Sequence[ast.If]:
if not has_else(node):
return []
negated_nodes = []
regular_nodes = []
for subnode in chain(node):
if not isinstance(subnode, ast.If) or subnode in self._visited_ifs:
continue
self._visited_ifs.add(subnode)
if self._is_negated_if_condition(subnode):
negated_nodes.append(subnode)
else:
regular_nodes.append(subnode)
if not regular_nodes and len(negated_nodes) > 1:
return [] # we allow all negated nodes in `if/elif/else`
return negated_nodes
def _process_ifexpr(self, node: ast.IfExp) -> Sequence[ast.IfExp]:
if not self._is_negated_if_condition(node):
return []
return [node]
def _is_negated_if_condition(self, node: AnyIf) -> bool:
return (
isinstance(node.test, ast.UnaryOp)
and isinstance(node.test.op, ast.Not)
) or (
isinstance(node.test, ast.Compare)
and all(
isinstance(elem, self._negated_ops) for elem in node.test.ops
)
)
import ast
from typing import List, NamedTuple
from typing import NamedTuple, final
from typing_extensions import final
from wemake_python_styleguide import constants

@@ -27,3 +25,3 @@ from wemake_python_styleguide.logic.naming import logical

"""
return '{0}{1}'.format(
return '{}{}'.format(
'.' * node.level,

@@ -34,8 +32,2 @@ node.module or '',

def get_import_parts(node: AnyImport) -> List[str]:
"""Returns list of import modules."""
module_path = getattr(node, 'module', '') or ''
return module_path.split('.')
def is_vague_import(name: str) -> bool:

@@ -59,18 +51,4 @@ """

blacklisted = name in constants.VAGUE_IMPORTS_BLACKLIST
with_from_or_to = (
name.startswith('from_') or
name.startswith('to_')
)
with_from_or_to = name.startswith(('from_', 'to_'))
too_short = logical.is_too_short_name(name, 2, trim=True)
return blacklisted or with_from_or_to or too_short
def is_nested_typing_import(parent: ast.AST) -> bool:
"""Tells whether ``if`` checks for ``TYPE_CHECKING``."""
checked_condition = None
if isinstance(parent, ast.If):
if isinstance(parent.test, ast.Name):
checked_condition = parent.test.id
elif isinstance(parent.test, ast.Attribute):
checked_condition = parent.test.attr
return checked_condition in constants.ALLOWED_NESTED_IMPORTS_CONDITIONS
import ast
from typing import List, Sequence, Tuple, Type, Union
from typing import TypeAlias
from wemake_python_styleguide.logic.nodes import get_context
_ReturningNodes = List[Union[ast.Return, ast.Yield]]
_ReturningNodes: TypeAlias = list[ast.Return | ast.Yield]

@@ -11,4 +11,4 @@

node: ast.AST,
returning_type: Union[Type[ast.Return], Type[ast.Yield]],
) -> Tuple[_ReturningNodes, bool]:
returning_type: type[ast.Return] | type[ast.Yield],
) -> tuple[_ReturningNodes, bool]:
"""Returns ``return`` or ``yield`` nodes with values."""

@@ -24,24 +24,1 @@ returns: _ReturningNodes = []

return returns, has_values
def is_simple_return(body: Sequence[ast.stmt]) -> bool:
"""Check if a statement only returns a boolean constant."""
if len(body) != 1:
return False
return _node_returns_bool_const(body[0])
def next_node_returns_bool(body: Sequence[ast.stmt], index: int) -> bool:
"""Check if the node after exiting the context returns a boolean const."""
if len(body) < index + 1:
return False
return _node_returns_bool_const(body[index])
def _node_returns_bool_const(node: ast.stmt) -> bool:
"""Checks if a Return node would return a boolean constant."""
return (
isinstance(node, ast.Return) and
isinstance(node.value, ast.NameConstant) and
isinstance(node.value.value, bool)
)
import ast
from typing import Optional

@@ -9,3 +8,3 @@ from wemake_python_styleguide.compat.aliases import ForNodes

def _does_loop_contain_node(
loop: Optional[AnyLoop],
loop: AnyLoop | None,
to_check: ast.AST,

@@ -39,3 +38,4 @@ ) -> bool:

is_nested_break = _does_loop_contain_node(
closest_loop, subnode,
closest_loop,
subnode,
)

@@ -42,0 +42,0 @@ if not is_nested_break:

import ast
from typing import Optional, Type

@@ -20,10 +19,3 @@ from wemake_python_styleguide.logic.nodes import get_parent

def unwrap_starred_node(node: ast.AST) -> ast.AST:
"""Unwraps the unary ``*`` starred node."""
if isinstance(node, ast.Starred):
return node.value
return node
def get_parent_ignoring_unary(node: ast.AST) -> Optional[ast.AST]:
def get_parent_ignoring_unary(node: ast.AST) -> ast.AST | None:
"""

@@ -49,3 +41,3 @@ Returns real parent ignoring proxy unary parent level.

node: ast.AST,
operator: Type[ast.unaryop],
operator: type[ast.unaryop],
amount: int = 0,

@@ -52,0 +44,0 @@ ) -> int:

@@ -1,11 +0,15 @@

from typing import Iterable
import ast
from collections.abc import Iterable
from wemake_python_styleguide.compat.nodes import Match, MatchAs, match_case
from wemake_python_styleguide.logic.nodes import get_parent
from wemake_python_styleguide.logic.tree import (
operators,
)
from wemake_python_styleguide.logic.walk import get_subnodes_by_type
from wemake_python_styleguide.logic.walrus import get_assigned_expr
def get_explicit_as_names(
node: Match,
) -> Iterable[MatchAs]: # pragma: py-lt-310
node: ast.Match,
) -> Iterable[ast.MatchAs]:
"""

@@ -17,5 +21,28 @@ Returns variable names defined as ``case ... as var_name``.

"""
for match_as in get_subnodes_by_type(node, MatchAs):
if isinstance(get_parent(match_as), match_case):
if match_as.pattern and match_as.name:
yield match_as
for match_as in get_subnodes_by_type(node, ast.MatchAs):
if (
isinstance(get_parent(match_as), ast.match_case)
and match_as.pattern
and match_as.name
):
yield match_as
def is_constant_subject(condition: ast.AST | list[ast.expr]) -> bool:
"""Detect constant subjects for `ast.Match` nodes."""
if isinstance(condition, list):
return all(is_constant_subject(node) for node in condition)
node = operators.unwrap_unary_node(get_assigned_expr(condition))
if isinstance(node, ast.Constant):
return True
if isinstance(node, ast.Tuple | ast.List | ast.Set):
return is_constant_subject(node.elts)
if isinstance(node, ast.Dict):
return (
not any(dict_key is None for dict_key in node.keys)
and is_constant_subject([
dict_key for dict_key in node.keys if dict_key is not None
])
and is_constant_subject(node.values)
)
return False

@@ -1,2 +0,2 @@

from ast import AST, Attribute, Call, ClassDef, walk
import ast

@@ -8,7 +8,7 @@ from wemake_python_styleguide.logic.nodes import get_context

def _is_self_call(func: AnyFunctionDef, node: AST) -> bool:
def _is_self_call(func: AnyFunctionDef, node: ast.AST) -> bool:
return (
isinstance(node, Call) and
isinstance(node.func, Attribute) and
bool(given_function_called(node, {'self.{0}'.format(func.name)}))
isinstance(node, ast.Call)
and isinstance(node.func, ast.Attribute)
and bool(given_function_called(node, {f'self.{func.name}'}))
)

@@ -18,15 +18,14 @@

def _check_method_recursion(func: AnyFunctionDef) -> bool:
return bool([
node
for node in walk(func)
if _is_self_call(func, node)
])
return bool([node for node in ast.walk(func) if _is_self_call(func, node)])
def _check_function_recursion(func: AnyFunctionDef) -> bool:
return bool([
node
for node in walk(func)
if isinstance(node, Call) and given_function_called(node, {func.name})
])
return bool(
[
node
for node in ast.walk(func)
if isinstance(node, ast.Call)
and given_function_called(node, {func.name})
],
)

@@ -40,4 +39,4 @@

"""
if isinstance(get_context(func), ClassDef):
if isinstance(get_context(func), ast.ClassDef):
return _check_method_recursion(func)
return _check_function_recursion(func)

@@ -13,4 +13,4 @@ import ast

return (
source.node_to_string(node.value) == iterable and
source.node_to_string(node.slice) == target
source.node_to_string(node.value) == iterable
and source.node_to_string(node.slice) == target
)

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

return False
return isinstance(node.value, ast.Str)
return isinstance(node.value, ast.Constant) and isinstance(
node.value.value,
str,
)

@@ -1,2 +0,2 @@

from ast import AST, Ellipsis, Expr, Raise, Str
import ast

@@ -17,9 +17,10 @@ from wemake_python_styleguide.types import AnyFunctionDef

"""
function_has_docstring = (
isinstance(node.body[0], Expr) and
isinstance(node.body[0].value, Str)
)
if function_has_docstring:
first_node = node.body[0]
if (
isinstance(first_node, ast.Expr)
and isinstance(first_node.value, ast.Constant)
and isinstance(first_node.value.value, str)
):
return _is_stub_with_docstring(node)
return _is_stub_without_docstring(node)
return _is_stub_without_docstring(first_node)

@@ -31,24 +32,17 @@

return True
elif statements_in_body == 2:
return (
_is_ellipsis(node.body[1]) or
isinstance(node.body[1], Raise)
)
if statements_in_body == 2:
second_node = node.body[1]
return _is_ellipsis(second_node) or isinstance(second_node, ast.Raise)
return False
def _is_stub_without_docstring(node: AnyFunctionDef) -> bool:
return (
len(node.body) == 1 and
(
_is_ellipsis(node.body[0]) or
isinstance(node.body[0], Raise)
)
)
def _is_stub_without_docstring(node: ast.AST) -> bool:
return _is_ellipsis(node) or isinstance(node, ast.Raise)
def _is_ellipsis(node: AST) -> bool:
def _is_ellipsis(node: ast.AST) -> bool:
return (
isinstance(node, Expr) and
isinstance(node.value, Ellipsis)
isinstance(node, ast.Expr)
and isinstance(node.value, ast.Constant)
and isinstance(node.value.value, type(...))
)
import ast
from typing import List, Union
from typing import TypeAlias
from typing_extensions import TypeAlias
from wemake_python_styleguide.logic import nodes
from wemake_python_styleguide.logic.naming import access
_VarDefinition: TypeAlias = Union[ast.AST, ast.expr]
_LocalVariable: TypeAlias = Union[ast.Name, ast.ExceptHandler]
_VarDefinition: TypeAlias = ast.AST | ast.expr
_LocalVariable: TypeAlias = ast.Name | ast.ExceptHandler

@@ -20,17 +17,2 @@

def does_shadow_builtin(node: ast.AST) -> bool:
"""
We allow attributes and class-level builtin overrides.
Like: ``self.list = []`` or ``def map(self, function):``
Why?
Because they cannot harm you since they do not shadow the real builtin.
"""
return (
not isinstance(node, ast.Attribute) and
not isinstance(nodes.get_context(node), ast.ClassDef)
)
def is_valid_block_variable_definition(node: _VarDefinition) -> bool:

@@ -49,6 +31,3 @@ """Is used to check either block variables are correctly defined."""

if isinstance(target, ast.Tuple):
return all(
_is_valid_single(element)
for element in target.elts
)
return all(_is_valid_single(element) for element in target.elts)
return _is_valid_single(target)

@@ -58,4 +37,3 @@

def _is_valid_single(node: _VarDefinition) -> bool:
return (
isinstance(node, ast.Name) or
return isinstance(node, ast.Name) or (
isinstance(node, ast.Starred) and isinstance(node.value, ast.Name)

@@ -65,14 +43,14 @@ )

def is_getting_element_by_unpacking(targets: List[ast.expr]) -> bool:
def is_getting_element_by_unpacking(targets: list[ast.expr]) -> bool:
"""Checks if unpacking targets used to get first or last element."""
if len(targets) != 2:
return False
first_item = (
isinstance(targets[1], ast.Starred) and
_is_unused_variable_name(targets[1].value)
)
last_item = (
isinstance(targets[0], ast.Starred) and
_is_unused_variable_name(targets[0].value)
)
first_item = isinstance(
targets[1],
ast.Starred,
) and _is_unused_variable_name(targets[1].value)
last_item = isinstance(
targets[0],
ast.Starred,
) and _is_unused_variable_name(targets[0].value)
return first_item or last_item

@@ -79,0 +57,0 @@

import ast
from typing import Iterator, Optional, Type, TypeVar, Union
from collections.abc import Iterator
from typing import TypeAlias, TypeVar
from typing_extensions import TypeAlias
from wemake_python_styleguide.logic.nodes import get_parent

@@ -10,3 +9,3 @@ from wemake_python_styleguide.types import AnyNodes

_SubnodeType = TypeVar('_SubnodeType', bound=ast.AST)
_IsInstanceContainer: TypeAlias = Union[AnyNodes, type]
_IsInstanceContainer: TypeAlias = AnyNodes | type

@@ -29,3 +28,3 @@

parents: _IsInstanceContainer,
) -> Optional[ast.AST]:
) -> ast.AST | None:
"""Returns the closes parent of a node of requested types."""

@@ -59,3 +58,3 @@ parent = get_parent(node)

node: ast.AST,
subnodes_type: Type[_SubnodeType],
subnodes_type: type[_SubnodeType],
) -> Iterator[_SubnodeType]:

@@ -62,0 +61,0 @@ """Returns the list of subnodes of given node with given subnode type."""

@@ -41,10 +41,2 @@ """

:str:`wemake_python_styleguide.options.defaults.MAX_NAME_LENGTH`
- ``i-control-code`` - whether you control ones who use your code,
more rules are enforced when you do control it,
opposite to ``--i-dont-control-code``, defaults to
:str:`wemake_python_styleguide.options.defaults.I_CONTROL_CODE`
- ``i-dont-control-code`` - whether you control ones who use your code,
more rules are enforced when you do control it,
opposite to ``--i-control-code``, defaults to
:str:`wemake_python_styleguide.options.defaults.I_CONTROL_CODE`
- ``nested-classes-whitelist`` - list of nested classes' names we allow to use,

@@ -60,2 +52,6 @@ defaults to

:str:`wemake_python_styleguide.options.defaults.FORBIDDEN_DOMAIN_NAMES`
- ``allowed-module-metadata`` - list of allowed module metadata, defaults to
:str:`wemake_python_styleguide.options.defaults.ALLOWED_MODULE_METADATA`
- ``forbidden-module-metadata`` - list of forbidden module metadata, defaults to
:str:`wemake_python_styleguide.options.defaults.FORBIDDEN_MODULE_METADATA`
- ``forbidden-inline-ignore`` - list of codes of violations or

@@ -134,2 +130,5 @@ class of violations that are forbidden to ignore inline, defaults to

:str:`wemake_python_styleguide.options.defaults.MAX_RAISES`
- ``max-except-exceptions`` - maximum number of exceptions in ``except``,
defaults to
:str:`wemake_python_styleguide.options.defaults.MAX_EXCEPT_EXCEPTIONS`
- ``max-cognitive-score`` - maximum amount of cognitive complexity

@@ -153,2 +152,11 @@ per function, defaults to

:str:`wemake_python_styleguide.options.defaults.MAX_TUPLE_UNPACK_LENGTH`
- ``max-type-params`` - maximum number of PEP695 type parameters,
defaults to
:str:`wemake_python_styleguide.options.defaults.MAX_TYPE_PARAMS`
- ``max-match-subjects`` - maximum number of subjects in a match statement,
defaults to
:str:`wemake_python_styleguide.options.defaults.MAX_MATCH_SUBJECTS`
- ``max-match-cases`` - maximum number of cases in a match block of code
defaults to
:str:`wemake_python_styleguide.options.defaults.MAX_MATCH_CASES`

@@ -162,7 +170,7 @@ .. rubric:: Formatter options

from typing import ClassVar, Mapping, Optional, Sequence, Union
from collections.abc import Mapping, Sequence
from typing import ClassVar, Final, TypeAlias, final
import attr
from flake8.options.manager import OptionManager
from typing_extensions import Final, TypeAlias, final

@@ -172,3 +180,3 @@ from wemake_python_styleguide.options import defaults

_Type: TypeAlias = type
ConfigValuesTypes: TypeAlias = Union[str, int, bool, Sequence[str]]
ConfigValuesTypes: TypeAlias = str | int | bool | Sequence[str]
String: Final = str

@@ -185,14 +193,14 @@

help: str # noqa: WPS125
type: Optional[_Type] = int # noqa: WPS125
type: _Type | None = int # noqa: WPS125
parse_from_config: bool = True
action: str = 'store'
comma_separated_list: bool = False
dest: Optional[str] = None
dest: str | None = None
def __attrs_post_init__(self) -> None:
"""Is called after regular init is done."""
object.__setattr__( # noqa: WPS609
self, 'help', ' '.join(
(self.help, 'Defaults to: %(default)s'), # noqa: WPS323
),
object.__setattr__(
self,
'help',
f'{self.help} Defaults to: %(default)s',
)

@@ -215,3 +223,2 @@

# General:
_Option(

@@ -228,19 +235,2 @@ '--min-name-length',

_Option(
'--i-control-code',
defaults.I_CONTROL_CODE,
'Whether you control ones who use your code.',
action='store_true',
type=None,
dest='i_control_code',
),
_Option(
'--i-dont-control-code',
defaults.I_CONTROL_CODE,
'Whether you control ones who use your code.',
action='store_false',
type=None,
dest='i_control_code',
parse_from_config=False,
),
_Option(
'--max-noqa-comments',

@@ -272,2 +262,16 @@ defaults.MAX_NOQA_COMMENTS,

_Option(
'--allowed-module-metadata',
defaults.ALLOWED_MODULE_METADATA,
'Names that are allowed as a module metadata attribute.',
type=String,
comma_separated_list=True,
),
_Option(
'--forbidden-module-metadata',
defaults.FORBIDDEN_MODULE_METADATA,
'Names that are disallowed as a module metadata attribute.',
type=String,
comma_separated_list=True,
),
_Option(
'--forbidden-inline-ignore',

@@ -284,5 +288,3 @@ defaults.FORBIDDEN_INLINE_IGNORE,

),
# Complexity:
_Option(

@@ -394,2 +396,7 @@ '--max-returns',

_Option(
'--max-except-exceptions',
defaults.MAX_EXCEPT_EXCEPTIONS,
'Maximum number of raises in a function.',
),
_Option(
'--max-cognitive-score',

@@ -424,5 +431,18 @@ defaults.MAX_COGNITIVE_SCORE,

),
_Option(
'--max-type-params',
defaults.MAX_TYPE_PARAMS,
'Maximum number of PEP695 type parameters.',
),
_Option(
'--max-match-subjects',
defaults.MAX_MATCH_SUBJECTS,
'Maximum number of subjects in a match statement.',
),
_Option(
'--max-match-cases',
defaults.MAX_MATCH_CASES,
'Maximum number of match cases in a single match.',
),
# Formatter:
_Option(

@@ -429,0 +449,0 @@ '--show-violation-links',

@@ -15,3 +15,3 @@ """

from typing_extensions import Final
from typing import Final

@@ -28,5 +28,2 @@ # ========

#: Whether you control ones who use your code.
I_CONTROL_CODE: Final = True
#: Maximum amount of ``noqa`` comments per module.

@@ -39,3 +36,3 @@ MAX_NOQA_COMMENTS: Final = 10 # guessed

'Params', # factoryboy specific
'Config', # pydantic spesific
'Config', # pydantic specific
)

@@ -55,2 +52,8 @@

#: List of module metadata we allow to use.
ALLOWED_MODULE_METADATA: Final = ()
#: List of module metadata we forbid to use.
FORBIDDEN_MODULE_METADATA: Final = ()
# ===========

@@ -123,2 +126,5 @@ # Complexity:

#: Maximum number of exceptions in `except`.
MAX_EXCEPT_EXCEPTIONS: Final = 3 # guessed
#: Maximum amount of cognitive complexity per function.

@@ -142,2 +148,11 @@ MAX_COGNITIVE_SCORE: Final = 12 # based on this code statistics

#: Maximum number of PEP695 type parameters.
MAX_TYPE_PARAMS: Final = 6 # 7-1, guessed
#: Maximum number of subjects in a ``match`` statement.
MAX_MATCH_SUBJECTS: Final = 7 # 7 +- 0, guessed
#: Maximum number of subjects in match statement.
MAX_MATCH_CASES: Final = 7 # guessed
# ==========

@@ -144,0 +159,0 @@ # Formatter:

@@ -1,15 +0,14 @@

from typing import Optional, Tuple
from typing import Any, final
import attr
from typing_extensions import final
from wemake_python_styleguide.options import defaults
from wemake_python_styleguide.types import ConfigurationOptions
def _min_max(
min: Optional[int] = None, # noqa: WPS125
max: Optional[int] = None, # noqa: WPS125
min: int | None = None, # noqa: A002
max: int | None = None, # noqa: A002
):
"""Validator to check that value is in bounds."""
def factory(instance, attribute, field_value):

@@ -19,6 +18,6 @@ min_contract = min is not None and field_value < min

if min_contract or max_contract:
raise ValueError('Option {0} is out of bounds: {1}'.format(
attribute.name,
field_value,
))
raise ValueError(
f'Option {attribute.name} is out of bounds: {field_value}',
)
return factory

@@ -28,4 +27,4 @@

def validate_domain_names_options(
allowed_domain_names: Tuple[str, ...],
forbidden_domain_names: Tuple[str, ...],
allowed_domain_names: tuple[str, ...],
forbidden_domain_names: tuple[str, ...],
) -> None:

@@ -45,6 +44,6 @@ """

(
'Names passed to `allowed_domain_name` and ' +
'`forbidden_domain_name` cannot intersect. ' +
'Intersecting names: ' +
', '.join(intersecting_names)
'Names passed to `allowed_domain_name` and '
+ '`forbidden_domain_name` cannot intersect. '
+ 'Intersecting names: '
+ ', '.join(intersecting_names)
),

@@ -56,3 +55,3 @@ )

@attr.dataclass(slots=True, frozen=True)
class _ValidatedOptions:
class ValidatedOptions:
"""

@@ -66,3 +65,2 @@ Here we write all the required structured validation for the options.

min_name_length: int = attr.ib(validator=[_min_max(min=1)])
i_control_code: bool
max_name_length: int = attr.ib(validator=[_min_max(min=1)])

@@ -72,6 +70,8 @@ max_noqa_comments: int = attr.ib(

)
nested_classes_whitelist: Tuple[str, ...] = attr.ib(converter=tuple)
allowed_domain_names: Tuple[str, ...] = attr.ib(converter=tuple)
forbidden_domain_names: Tuple[str, ...] = attr.ib(converter=tuple)
forbidden_inline_ignore: Tuple[str, ...] = attr.ib(converter=tuple)
nested_classes_whitelist: tuple[str, ...] = attr.ib(converter=tuple)
allowed_domain_names: tuple[str, ...] = attr.ib(converter=tuple)
forbidden_domain_names: tuple[str, ...] = attr.ib(converter=tuple)
allowed_module_metadata: tuple[str, ...] = attr.ib(converter=tuple)
forbidden_module_metadata: tuple[str, ...] = attr.ib(converter=tuple)
forbidden_inline_ignore: tuple[str, ...] = attr.ib(converter=tuple)

@@ -100,2 +100,3 @@ # Complexity:

max_raises: int = attr.ib(validator=[_min_max(min=1)])
max_except_exceptions: int = attr.ib(validator=[_min_max(min=1)])
max_cognitive_score: int = attr.ib(validator=[_min_max(min=1)])

@@ -107,2 +108,5 @@ max_cognitive_average: int = attr.ib(validator=[_min_max(min=1)])

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

@@ -112,3 +116,3 @@ exps_for_one_empty_line: int

def validate_options(options: ConfigurationOptions) -> _ValidatedOptions:
def validate_options(options: Any) -> ValidatedOptions:
"""Validates all options from ``flake8``, uses a subset of them."""

@@ -119,11 +123,7 @@ validate_domain_names_options(

)
fields_to_validate = [
field.name
for field in attr.fields(_ValidatedOptions)
]
fields_to_validate = [field.name for field in attr.fields(ValidatedOptions)]
options_subset = {
field: getattr(options, field, None)
for field in fields_to_validate
field: getattr(options, field, None) for field in fields_to_validate
}
# Next line raises `TypeError` if `options_subset` is invalid.
return _ValidatedOptions(**options_subset) # type: ignore
return ValidatedOptions(**options_subset) # type: ignore[arg-type]

@@ -1,2 +0,2 @@

from typing_extensions import Final
from typing import Final

@@ -15,2 +15,3 @@ from wemake_python_styleguide.visitors.ast.complexity import ( # noqa: WPS235

overuses,
pm,
)

@@ -22,11 +23,6 @@

function.CognitiveComplexityVisitor,
imports.ImportMembersVisitor,
jones.JonesComplexityVisitor,
nested.NestedComplexityVisitor,
offset.OffsetVisitor,
counts.ModuleMembersVisitor,

@@ -38,14 +34,12 @@ counts.ConditionsVisitor,

counts.TupleUnpackVisitor,
counts.TypeParamsVisitor,
classes.ClassComplexityVisitor,
classes.MethodMembersVisitor,
overuses.StringOveruseVisitor,
overuses.ExpressionOveruseVisitor,
access.AccessVisitor,
calls.CallChainsVisitor,
annotations.AnnotationComplexityVisitor,
pm.MatchSubjectsVisitor,
pm.MatchCasesVisitor,
)

@@ -1,2 +0,2 @@

from typing_extensions import Final
from typing import Final

@@ -8,7 +8,5 @@ from wemake_python_styleguide.visitors.ast.naming import validation, variables

validation.WrongNameVisitor,
variables.WrongModuleMetadataVisitor,
variables.WrongVariableAssignmentVisitor,
variables.UnusedVariableUsageVisitor,
variables.UnusedVariableDefinitionVisitor,
)

@@ -1,2 +0,2 @@

from typing_extensions import Final
from typing import Final

@@ -17,15 +17,7 @@ from wemake_python_styleguide.visitors.tokenize import (

comments.EmptyCommentVisitor,
syntax.WrongKeywordTokenVisitor,
primitives.WrongNumberTokenVisitor,
primitives.WrongStringTokenVisitor,
primitives.WrongStringConcatenationVisitor,
statements.ExtraIndentationVisitor,
statements.BracketLocationVisitor,
statements.MultilineStringVisitor,
statements.InconsistentComprehensionVisitor,
conditions.IfElseVisitor,
)

@@ -1,2 +0,2 @@

from typing_extensions import Final
from typing import Final

@@ -8,4 +8,2 @@ from wemake_python_styleguide.visitors.filenames.module import (

#: Here we define all filename-based visitors.
PRESET: Final = (
WrongModuleNameVisitor,
)
PRESET: Final = (WrongModuleNameVisitor,)

@@ -1,7 +0,5 @@

from typing_extensions import Final
from typing import Final
from wemake_python_styleguide.presets.topics import complexity, naming
from wemake_python_styleguide.visitors.ast import ( # noqa: WPS235
annotations,
attributes,
blocks,

@@ -21,2 +19,3 @@ builtins,

operators,
pm,
redundancy,

@@ -34,3 +33,2 @@ statements,

statements.StatementsWithBodiesVisitor,
statements.WrongParametersIndentationVisitor,
statements.PointlessStarredVisitor,

@@ -40,3 +38,2 @@ statements.WrongNamedKeywordVisitor,

statements.WrongMethodArgumentsVisitor,
keywords.WrongRaiseVisitor,

@@ -46,14 +43,7 @@ keywords.WrongKeywordVisitor,

keywords.ConsistentReturningVisitor,
keywords.ConsistentReturningVariableVisitor,
keywords.ConstantKeywordVisitor,
keywords.GeneratorKeywordsVisitor,
loops.WrongComprehensionVisitor,
loops.WrongLoopVisitor,
loops.WrongLoopDefinitionVisitor,
loops.SyncForLoopVisitor,
attributes.WrongAttributeVisitor,
annotations.WrongAnnotationVisitor,
functions.WrongFunctionCallVisitor,

@@ -63,13 +53,8 @@ functions.FunctionDefinitionVisitor,

functions.WrongFunctionCallContextVisitor,
functions.UnnecessaryLiteralsVisitor,
functions.FunctionSignatureVisitor,
functions.FloatingNanCallVisitor,
tokenize_functions.WrongEmptyLinesCountVisitor,
exceptions.WrongTryExceptVisitor,
exceptions.NestedTryBlocksVisitor,
exceptions.WrongExceptHandlerVisitor,
imports.WrongImportVisitor,
builtins.WrongNumberVisitor,

@@ -80,24 +65,15 @@ builtins.WrongStringVisitor,

builtins.WrongCollectionVisitor,
operators.UselessOperatorsVisitor,
operators.WrongMathOperatorVisitor,
operators.WalrusVisitor,
operators.BitwiseOpVisitor,
compares.WrongConditionalVisitor,
compares.CompareSanityVisitor,
compares.WrongComparisonOrderVisitor,
compares.UnaryCompareVisitor,
compares.WrongConstantCompareVisitor,
compares.InCompareSanityVisitor,
compares.WrongFloatComplexCompareVisitor,
conditions.IfStatementVisitor,
conditions.BooleanConditionVisitor,
conditions.ImplicitBoolPatternsVisitor,
conditions.UselessElseVisitor,
conditions.MatchVisitor,
conditions.ChainedIsVisitor,
iterables.IterableUnpackingVisitor,
classes.WrongClassDefVisitor,

@@ -110,14 +86,9 @@ classes.WrongClassBodyVisitor,

classes.BuggySuperCallVisitor,
blocks.BlockVariableVisitor,
blocks.AfterBlockVariablesVisitor,
subscripts.SubscriptVisitor,
subscripts.ImplicitDictGetVisitor,
subscripts.CorrectKeyVisitor,
decorators.WrongDecoratorVisitor,
redundancy.RedundantEnumerateVisitor,
pm.MatchSubjectVisitor,
# Modules:

@@ -127,3 +98,2 @@ modules.EmptyModuleContentsVisitor,

modules.ModuleConstantsVisitor,
# Topics:

@@ -130,0 +100,0 @@ *complexity.PRESET,

import ast
from pep8ext_naming import NamingChecker
from typing_extensions import final
from wemake_python_styleguide.transformations.ast.bugfixes import (
fix_line_number,
)
from wemake_python_styleguide.transformations.ast.enhancements import (
set_constant_evaluations,
set_if_chain,
set_node_context,

@@ -16,15 +8,2 @@ )

@final
class _ClassVisitor(ast.NodeVisitor):
"""Used to set method types inside classes."""
def __init__(self, transformer: NamingChecker) -> None:
super().__init__()
self.transformer = transformer
def visit_ClassDef(self, node: ast.ClassDef) -> None: # noqa: N802
self.transformer.tag_class_functions(node)
self.generic_visit(node)
def _set_parent(tree: ast.AST) -> ast.AST:

@@ -51,16 +30,2 @@ """

def _set_function_type(tree: ast.AST) -> ast.AST:
"""
Sets the function type for methods.
Can set: `method`, `classmethod`, `staticmethod`.
.. versionchanged:: 0.3.0
"""
transformer = _ClassVisitor(NamingChecker(tree, 'stdin'))
transformer.visit(tree)
return tree
def transform(tree: ast.AST) -> ast.AST:

@@ -81,11 +46,4 @@ """

_set_parent,
_set_function_type,
# Bugfixes, order is not important:
fix_line_number,
# Enhancements, order is not important:
set_node_context,
set_if_chain,
set_constant_evaluations,
)

@@ -92,0 +50,0 @@

import ast
import operator
from contextlib import suppress
from types import MappingProxyType
from typing import Optional, Tuple, Type, Union
from typing import Final
from typing_extensions import Final
from wemake_python_styleguide.compat.aliases import FunctionNodes
from wemake_python_styleguide.logic.nodes import evaluate_node, get_parent
from wemake_python_styleguide.logic.nodes import get_parent
from wemake_python_styleguide.types import ContextNodes
_CONTEXTS: Tuple[Type[ContextNodes], ...] = (
_CONTEXTS: Final = (
ast.Module,

@@ -19,46 +14,3 @@ ast.ClassDef,

_AST_OPS_TO_OPERATORS: Final = MappingProxyType({
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: operator.mul,
ast.Div: operator.truediv,
ast.FloorDiv: operator.floordiv,
ast.Mod: operator.mod,
ast.Pow: operator.pow,
ast.LShift: operator.lshift,
ast.RShift: operator.rshift,
ast.BitAnd: operator.and_,
ast.BitOr: operator.or_,
ast.BitXor: operator.xor,
})
def set_if_chain(tree: ast.AST) -> ast.AST:
"""
Used to create ``if`` chains.
We have a problem, because we cannot tell which situation is happening:
.. code:: python
if some_value:
if other_value:
...
.. code:: python
if some_value:
...
elif other_value:
...
Since they are very similar it very hard to make a different when
actually working with nodes. So, we need a simple way to separate them.
"""
for statement in ast.walk(tree):
if isinstance(statement, ast.If):
_apply_if_statement(statement)
return tree
def set_node_context(tree: ast.AST) -> ast.AST:

@@ -95,31 +47,6 @@ """

def set_constant_evaluations(tree: ast.AST) -> ast.AST:
"""
Used to evaluate operations between constants.
We want this to be able to analyze parts of the code in which a math
operation is making the linter unable to understand if the code is
compliant or not.
Example:
.. code:: python
value = array[1 + 0.5]
This should not be allowed, because we would be using a float to index an
array, but since there is an addition, the linter does not know that and
does not raise an error.
"""
for stmt in ast.walk(tree):
parent = get_parent(stmt)
if isinstance(stmt, ast.BinOp) and not isinstance(parent, ast.BinOp):
evaluation = evaluate_operation(stmt)
setattr(stmt, 'wps_op_eval', evaluation) # noqa: B010
return tree
def _find_context(
node: ast.AST,
contexts: Tuple[Type[ast.AST], ...],
) -> Optional[ast.AST]:
contexts: tuple[type[ContextNodes], ...],
) -> ast.AST | None:
"""

@@ -134,37 +61,4 @@ We changed how we find and assign contexts in 0.8.1 version.

return None
elif isinstance(parent, contexts):
if isinstance(parent, contexts):
return parent
return _find_context(parent, contexts)
def _apply_if_statement(statement: ast.If) -> None:
"""We need to add extra properties to ``if`` conditions."""
for child in ast.iter_child_nodes(statement):
if isinstance(child, ast.If):
if child in statement.orelse:
setattr(statement, 'wps_if_chained', True) # noqa: B010
setattr(child, 'wps_if_chain', statement) # noqa: B010
def evaluate_operation(
statement: ast.BinOp,
) -> Optional[Union[int, float, str, bytes]]:
"""Tries to evaluate all math operations inside the statement."""
if isinstance(statement.left, ast.BinOp):
left = evaluate_operation(statement.left)
else:
left = evaluate_node(statement.left)
if isinstance(statement.right, ast.BinOp):
right = evaluate_operation(statement.right)
else:
right = evaluate_node(statement.right)
op = _AST_OPS_TO_OPERATORS.get(type(statement.op))
evaluation = None
if op is not None:
with suppress(Exception):
evaluation = op(left, right)
return evaluation

@@ -39,244 +39,55 @@ """

import ast
from typing import Tuple, Type, Union
from typing import TypeAlias
from typing_extensions import Protocol, TypeAlias
#: We use this type to represent all string-like nodes.
AnyText: TypeAlias = Union[ast.Str, ast.Bytes]
#: In cases we need to work with both import types.
AnyImport: TypeAlias = Union[ast.Import, ast.ImportFrom]
AnyImport: TypeAlias = ast.Import | ast.ImportFrom
#: In cases we need to work with both function definitions.
AnyFunctionDef: TypeAlias = Union[ast.FunctionDef, ast.AsyncFunctionDef]
AnyFunctionDef: TypeAlias = ast.FunctionDef | ast.AsyncFunctionDef
#: In cases we need to work with all function definitions (including lambdas).
AnyFunctionDefAndLambda: TypeAlias = Union[AnyFunctionDef, ast.Lambda]
AnyFunctionDefAndLambda: TypeAlias = AnyFunctionDef | ast.Lambda
#: In cases we need to work with both forms of if functions.
AnyIf: TypeAlias = Union[ast.If, ast.IfExp]
AnyIf: TypeAlias = ast.If | ast.IfExp
#: In cases we need to work with both sync and async loops.
AnyFor: TypeAlias = Union[ast.For, ast.AsyncFor]
AnyFor: TypeAlias = ast.For | ast.AsyncFor
#: In case we need to work with any loop: sync, async, and while.
AnyLoop: TypeAlias = Union[AnyFor, ast.While]
AnyLoop: TypeAlias = AnyFor | ast.While
#: This is how you can define a variable in Python.
AnyVariableDef: TypeAlias = Union[ast.Name, ast.Attribute, ast.ExceptHandler]
AnyVariableDef: TypeAlias = ast.Name | ast.Attribute | ast.ExceptHandler
#: All different comprehension types in one place.
AnyComprehension: TypeAlias = Union[
ast.ListComp,
ast.DictComp,
ast.SetComp,
ast.GeneratorExp,
]
AnyComprehension: TypeAlias = (
ast.ListComp | ast.DictComp | ast.SetComp | ast.GeneratorExp
)
#: In cases we need to work with both sync and async context managers.
AnyWith: TypeAlias = Union[ast.With, ast.AsyncWith]
AnyWith: TypeAlias = ast.With | ast.AsyncWith
#: When we search for assign elements, we also need typed assign.
AnyAssign: TypeAlias = Union[ast.Assign, ast.AnnAssign]
AnyAssign: TypeAlias = ast.Assign | ast.AnnAssign
#: When we search for assign elements, we also need typed assign.
AnyAssignWithWalrus: TypeAlias = Union[AnyAssign, ast.NamedExpr]
AnyAssignWithWalrus: TypeAlias = AnyAssign | ast.NamedExpr
#: In cases we need to work with both access types.
AnyAccess: TypeAlias = Union[
ast.Attribute,
ast.Subscript,
]
AnyAccess: TypeAlias = ast.Attribute | ast.Subscript
#: In case we need to handle types that can be chained.
AnyChainable: TypeAlias = Union[
ast.Attribute,
ast.Subscript,
ast.Call,
]
AnyChainable: TypeAlias = ast.Attribute | ast.Subscript | ast.Call
#: Tuple of AST node types for declarative syntax.
AnyNodes: TypeAlias = Tuple[Type[ast.AST], ...]
AnyNodes: TypeAlias = tuple[type[ast.AST], ...]
#: We use this type to work with any text-like values. Related to `AnyText`.
AnyTextPrimitive: TypeAlias = Union[str, bytes]
AnyTextPrimitive: TypeAlias = str | bytes
#: That's how we define context of operations.
ContextNodes: TypeAlias = Union[
ast.Module,
ast.ClassDef,
AnyFunctionDef,
]
ContextNodes: TypeAlias = ast.Module | ast.ClassDef | AnyFunctionDef
#: Flake8 API format to return error messages.
CheckResult: TypeAlias = Tuple[int, int, str, type]
class ConfigurationOptions(Protocol):
"""
Provides structure for the options we use in our checker and visitors.
Then this protocol is passed to each individual visitor.
It uses structural sub-typing, and does not represent any kind of a real
class or structure.
We use ``@property`` decorator here instead of regular attributes,
because we need to explicitly mark these atrtibutes as read-only.
See also:
https://mypy.readthedocs.io/en/latest/protocols.html
"""
def __hash__(self) -> int:
"""We need these options to be hashable."""
# General:
@property
def min_name_length(self) -> int:
...
@property
def i_control_code(self) -> bool:
...
@property
def max_name_length(self) -> int:
...
@property
def max_noqa_comments(self) -> int:
...
@property
def nested_classes_whitelist(self) -> Tuple[str, ...]:
...
@property
def forbidden_inline_ignore(self) -> Tuple[str, ...]:
...
@property
def allowed_domain_names(self) -> Tuple[str, ...]:
...
@property
def forbidden_domain_names(self) -> Tuple[str, ...]:
...
# Complexity:
@property
def max_arguments(self) -> int:
...
@property
def max_local_variables(self) -> int:
...
@property
def max_returns(self) -> int:
...
@property
def max_expressions(self) -> int:
...
@property
def max_module_members(self) -> int:
...
@property
def max_methods(self) -> int:
...
@property
def max_line_complexity(self) -> int:
...
@property
def max_jones_score(self) -> int:
...
@property
def max_imports(self) -> int:
...
@property
def max_imported_names(self) -> int:
...
@property
def max_base_classes(self) -> int:
...
@property
def max_decorators(self) -> int:
...
@property
def max_string_usages(self) -> int:
...
@property
def max_awaits(self) -> int:
...
@property
def max_try_body_length(self) -> int:
...
@property
def max_module_expressions(self) -> int:
...
@property
def max_function_expressions(self) -> int:
...
@property
def max_asserts(self) -> int:
...
@property
def max_access_level(self) -> int:
...
@property
def max_attributes(self) -> int:
...
@property
def max_raises(self) -> int:
...
@property
def max_cognitive_score(self) -> int:
...
@property
def max_cognitive_average(self) -> int:
...
@property
def max_call_level(self) -> int:
...
@property
def max_annotation_complexity(self) -> int:
...
@property
def max_import_from_members(self) -> int:
...
@property
def max_tuple_unpack_length(self) -> int:
...
@property
def show_violation_links(self) -> bool:
...
@property
def exps_for_one_empty_line(self) -> int:
...
CheckResult: TypeAlias = tuple[int, int, str, type]

@@ -1,2 +0,3 @@

import os
from pathlib import Path
from typing import Final

@@ -6,5 +7,5 @@ from wemake_python_styleguide.compat.packaging import get_version

#: This is a package name. It is basically the name of the root folder.
pkg_name = os.path.basename(os.path.dirname(__file__))
pkg_name: Final = str(Path(__file__).parent.name)
#: We store the version number inside the `pyproject.toml`.
pkg_version = get_version(pkg_name)
pkg_version: Final = get_version(pkg_name)

@@ -38,4 +38,4 @@ """

When you want to mark some violation as deprecated,
then assign ``deprecated`` boolean flag to it:
When you want to mark some violation as deprecated and disabled,
then assign ``disabled_since`` with a string version number to it:

@@ -46,3 +46,3 @@ .. code:: python

class SomeViolation(ASTViolation):
deprecated = True
disabled_since = '1.0.0'

@@ -58,12 +58,7 @@ Reference

import tokenize
from typing import Callable, ClassVar, Optional, Set, Tuple, Union
from collections.abc import Callable
from typing import ClassVar, TypeAlias, final
from typing_extensions import TypeAlias, final
#: General type for all possible nodes where error happens.
ErrorNode: TypeAlias = Union[
ast.AST,
tokenize.TokenInfo,
None,
]
ErrorNode: TypeAlias = ast.AST | tokenize.TokenInfo | None

@@ -82,4 +77,3 @@ #: We use this type to define helper classes with callbacks to add violations.

# TODO: remove `noqa` after a new release (0.17.0):
class BaseViolation(metaclass=abc.ABCMeta): # noqa: WPS338
class BaseViolation(abc.ABC):
"""

@@ -96,4 +90,3 @@ Abstract base class for all style violations.

code: unique violation number. Used to identify the violation.
previous_codes: just a documentation thing to track changes in time.
deprecated: indicates that this violation will be removed soon.
disabled_since: indicates that this violation is disabled.
postfix_template: indicates message that we show at the very end.

@@ -105,4 +98,3 @@

code: ClassVar[int]
previous_codes: ClassVar[Set[int]]
deprecated: ClassVar[bool] = False
disabled_since: ClassVar[str | None] = None

@@ -126,3 +118,3 @@ # assigned in __init_subclass__

raise TypeError(
'Please include a docstring documenting {0}'.format(cls),
f'Please include a docstring documenting {cls}',
)

@@ -135,3 +127,4 @@ # this is mostly done for docs to display the full code,

cls.__doc__ = _prepend_skipping_whitespaces(
'{0} — '.format(cls.full_code), cls.__doc__,
f'{cls.full_code} — ',
cls.__doc__,
)

@@ -142,4 +135,4 @@

node: ErrorNode,
text: Optional[str] = None,
baseline: Optional[int] = None,
text: str | None = None,
baseline: int | None = None,
) -> None:

@@ -169,10 +162,6 @@ """

raise ValueError('Error message was not formatted', self)
return '{0} {1}{2}'.format(
self.full_code,
formatted,
self._postfix_information(),
)
return f'{self.full_code} {formatted}{self._postfix_information()}'
@final
def node_items(self) -> Tuple[int, int, str]:
def node_items(self) -> tuple[int, int, str]:
"""Returns tuple to match ``flake8`` API format."""

@@ -190,3 +179,4 @@ return (*self._location(), self.message())

"""
return 'WPS{0}'.format(str(cls.code).zfill(3))
code_part = str(cls.code).zfill(3)
return f'WPS{code_part}'

@@ -205,3 +195,3 @@ @final

@abc.abstractmethod
def _location(self) -> Tuple[int, int]:
def _location(self) -> tuple[int, int]:
"""Base method for showing error location."""

@@ -213,6 +203,6 @@

_node: Optional[ast.AST]
_node: ast.AST | None
@final
def _location(self) -> Tuple[int, int]:
def _location(self) -> tuple[int, int]:
line_number = getattr(self._node, 'lineno', 0)

@@ -239,5 +229,5 @@ column_offset = getattr(self._node, 'col_offset', 0)

self,
node: Optional[ast.AST] = None,
text: Optional[str] = None,
baseline: Optional[int] = None,
node: ast.AST | None = None,
text: str | None = None,
baseline: int | None = None,
) -> None:

@@ -254,3 +244,3 @@ """Creates new instance of module violation without explicit node."""

@final
def _location(self) -> Tuple[int, int]:
def _location(self) -> tuple[int, int]:
return self._node.start

@@ -267,4 +257,4 @@

node=None,
text: Optional[str] = None,
baseline: Optional[int] = None,
text: str | None = None,
baseline: int | None = None,
) -> None:

@@ -275,3 +265,3 @@ """Creates new instance of simple style violation."""

@final
def _location(self) -> Tuple[int, int]:
def _location(self) -> tuple[int, int]:
"""

@@ -288,3 +278,3 @@ Return violation location inside the file.

lstripped_text = text.lstrip()
leading_whitespaces = text[:len(text) - len(lstripped_text)]
leading_whitespaces = text[: len(text) - len(lstripped_text)]
return leading_whitespaces + prefix + lstripped_text

@@ -62,2 +62,6 @@ """

TooManyRaisesViolation
TooManyExceptExceptionsViolation
TooManyTypeParamsViolation
TooManyMatchSubjectsViolation
TooManyMatchCaseViolation

@@ -105,6 +109,10 @@ Module complexity

.. autoclass:: TooManyRaisesViolation
.. autoclass:: TooManyExceptExceptionsViolation
.. autoclass:: TooManyTypeParamsViolation
.. autoclass:: TooManyMatchSubjectsViolation
.. autoclass:: TooManyMatchCaseViolation
"""
from typing_extensions import final
from typing import final

@@ -314,2 +322,3 @@ from wemake_python_styleguide.violations.base import (

@final

@@ -385,3 +394,7 @@ class TooManyLocalsViolation(ASTViolation):

.. versionadded:: 0.1.0
.. versionchanged:: 1.0.0
Does not count special ``self``, ``cls``, and ``mcs`` as parameters.
Also does not count parameters in ``@overload`` definitions.
"""

@@ -1007,2 +1020,4 @@

.. versionadded:: 0.12.0
.. versionchanged:: 1.0.0
Any amount of attributes are allowed on ``@dataclasses``.

@@ -1216,6 +1231,4 @@ """

string as the key
- the return value of a procedure call without arguments
- the return value of a function / method call with 3 arguments maximum
Related to :class:`~FormattedStringViolation`.
Reasoning:

@@ -1235,5 +1248,7 @@ Complex ``f`` strings are often difficult to understand,

f'smth {user.get_full_name()}'
f'smth {math_func(1, 2, 3)}'
# Wrong:
f'{reverse("url-name")}?{"&".join("user="+uid for uid in user_ids)}'
f'{reverse("url-name")}?{"&".join("user=" + uid for uid in user_ids)}'
f'smth {math_func(1, 2, 3, 4)}'

@@ -1274,1 +1289,114 @@ .. versionadded:: 0.15.0

code = 238
@final
class TooManyExceptExceptionsViolation(ASTViolation):
"""
Forbids to have too many exceptions in ``except`` statement.
Reasoning:
Too exceptions in ``except`` case means
that too many things are happening here at once.
Solution:
Use common base classes, split ``except`` cases.
Configuration:
This rule is configurable with ``--max-except-exceptions``.
Default:
:str:`wemake_python_styleguide.options.defaults.MAX_EXCEPT_EXCEPTIONS`
.. versionadded:: 1.0.0
"""
error_template = 'Found too many exceptions in `except` case: {0}'
code = 239
@final
class TooManyTypeParamsViolation(ASTViolation):
"""
Forbids to have too many type params.
Is only emitted on ``python3.12+``.
Reasoning:
Too many type params means that you are probably overly complicate
the object that you are typing right now.
It would be really hard for users
to manually add all generic parameters.
Solution:
Use composition of classes, simplify the API.
Configuration:
This rule is configurable with ``--max-type-params``.
Default:
:str:`wemake_python_styleguide.options.defaults.MAX_TYPE_PARAMS`
.. versionadded:: 1.0.0
"""
error_template = 'Found too many type params: {0}'
code = 240
@final
class TooManyMatchSubjectsViolation(ASTViolation):
"""
Forbids to have too many subjects in ``match`` statements.
Reasoning:
Too many subjects in a ``match`` statement make the code
difficult to read and maintain. It indicates that the logic
could be simplified or broken down into smaller components.
Solution:
Refactor the ``match`` statement to reduce the number of subjects.
Consider splitting the logic into multiple ``match`` statements
or functions to improve clarity and maintainability.
Configuration:
This rule is configurable with ``--max-match-subjects``.
Default:
:str:`wemake_python_styleguide.options.defaults.MAX_MATCH_SUBJECTS`
.. versionadded:: 1.0.0
"""
error_template = 'Found too many subjects in `match` statement: {0}'
code = 241
@final
class TooManyMatchCaseViolation(ASTViolation):
"""
Forbids to have too many match cases.
Reasoning:
Too many match cases means that you are probably overly complicate
the object that you are matching right now.
It would be really hard for users
to manually add all match cases.
Solution:
Refactor the ``match`` statement by breaking the logic into smaller,
focused functions. This will improve readability and maintainability.
Split complex logic into separate functions to keep each one concise,
reducing the size of the ``match`` block and making the code easier
to understand and modify.
Configuration:
This rule is configurable with ``--max-match-cases``.
Default:
:str:`wemake_python_styleguide.options.defaults.MAX_MATCH_CASES`
.. versionadded:: 1.0.0
"""
error_template = 'Found too many cases in `match` block: {0}'
code = 242

@@ -48,3 +48,3 @@ """

UppercaseStringModifierViolation
WrongMultilineStringViolation
UselessMultilineStringViolation
ModuloStringFormatViolation

@@ -90,2 +90,3 @@ InconsistentReturnViolation

AssignToSliceViolation
RaiseSystemExitViolation

@@ -117,3 +118,3 @@ Consistency checks

.. autoclass:: UppercaseStringModifierViolation
.. autoclass:: WrongMultilineStringViolation
.. autoclass:: UselessMultilineStringViolation
.. autoclass:: ModuloStringFormatViolation

@@ -159,6 +160,7 @@ .. autoclass:: InconsistentReturnViolation

.. autoclass:: AssignToSliceViolation
.. autoclass:: RaiseSystemExitViolation
"""
from typing_extensions import final
from typing import final

@@ -251,2 +253,5 @@ from wemake_python_styleguide.violations.base import (

.. versionadded:: 0.1.0
.. versionchanged:: 1.0.0
No longer produced, kept here for historic reasons.
This rule is covered by ``ruff`` linter. See ``UP025``.

@@ -257,2 +262,3 @@ """

error_template = 'Found unicode string prefix: {0}'
disabled_since = '1.0.0'

@@ -271,7 +277,8 @@

Currently, it all depends on the cultural habits of the author.
We enforce a single way to write numbers: without the underscore.
We enforce a single way to write numbers with thousands separators.
We allow only underscores to be used as thousands separators.
Solution:
Numbers should be written as numbers: ``1000``.
If you have a very big number with a lot of zeros, use multiplication.
Using underscores as thousands separators if necessary.

@@ -282,9 +289,14 @@ Example::

phone = 88313443
million = 1000000
million = 1_000_000.50_001
hexed = 1_234.157_000e-1_123
binary = 0b1_001_001
# Wrong:
phone = 8_83_134_43
million = 100_00_00
million = 100_00_00.1_0
octal = 0o00_11
.. versionadded:: 0.1.0
.. versionchanged:: 1.0.0
Underscore ``_`` is now allowed with exactly 3 digits after it.

@@ -322,2 +334,5 @@ """

.. versionadded:: 0.1.0
.. versionchanged:: 1.0.0
No longer produced, kept here for historic reasons.
This is covered with ``ruff`` formatter.

@@ -328,2 +343,3 @@ """

error_template = 'Found partial float: {0}'
disabled_since = '1.0.0'

@@ -362,2 +378,5 @@

.. versionadded:: 0.1.0
.. versionchanged:: 1.0.0
No longer produced, kept here for historic reasons.
This is covered with ``ruff`` linter. See ``WPS237``.

@@ -368,2 +387,3 @@ """

code = 305
disabled_since = '1.0.0'

@@ -402,2 +422,5 @@

See PEP695 for extra reasoning.
.. versionchanged:: 1.0.0
No longer produced, kept here for historic reasons.
This is covered with ``ruff`` formatter.

@@ -408,2 +431,3 @@ """

code = 306
disabled_since = '1.0.0'

@@ -437,3 +461,3 @@

error_template = 'Found list comprehension with multiple `if`s'
error_template = 'Found a comprehension with multiple `if`s'
code = 307

@@ -497,2 +521,5 @@

.. versionadded:: 0.3.0
.. versionchanged:: 1.0.0
No longer produced, kept here for historic reasons.
This is covered with ``ruff`` linter. See ``SIM300``.

@@ -503,2 +530,3 @@ """

code = 309
disabled_since = '1.0.0'

@@ -537,2 +565,5 @@

.. versionadded:: 0.3.0
.. versionchanged:: 1.0.0
No longer produced, kept here for historic reasons.
This is covered with ``ruff`` formatter.

@@ -543,2 +574,3 @@ """

code = 310
disabled_since = '1.0.0'

@@ -637,2 +669,5 @@

.. versionadded:: 0.3.0
.. versionchanged:: 1.0.0
No longer produced, kept here for historic reasons.
This is covered with ``ruff`` formatter.

@@ -643,2 +678,3 @@ """

code = 313
disabled_since = '1.0.0'

@@ -649,3 +685,3 @@

"""
Forbid using ``if`` statements that use invalid conditionals.
Forbid using ``if`` or ``match`` statements that use invalid conditionals.

@@ -664,5 +700,10 @@ Reasoning:

if value is True: ...
match value:
case True:
...
# Wrong:
if True: ...
match True:
case True: ...

@@ -701,2 +742,5 @@ .. versionadded:: 0.3.0

.. versionadded:: 0.3.0
.. versionchanged:: 1.0.0
No longer produced, kept here for historic reasons.
This is covered with ``ruff`` linter. See ``UP004``.

@@ -707,2 +751,3 @@ """

code = 315
disabled_since = '1.0.0'

@@ -737,2 +782,5 @@

.. versionadded:: 0.6.0
.. versionchanged:: 1.0.0
No longer produced, kept here for historic reasons.
This is covered with ``ruff`` formatter. See ``SIM117``.

@@ -743,2 +791,3 @@ """

code = 316
disabled_since = '1.0.0'

@@ -815,2 +864,5 @@

.. versionadded:: 0.6.0
.. versionchanged:: 1.0.0
No longer produced, kept here for historic reasons.
This is covered with ``ruff`` formatter.

@@ -821,2 +873,3 @@ """

code = 317
disabled_since = '1.0.0'

@@ -858,2 +911,5 @@

.. versionadded:: 0.6.0
.. versionchanged:: 1.0.0
No longer produced, kept here for historic reasons.
This is covered with ``ruff`` formatter.

@@ -864,2 +920,3 @@ """

code = 318
disabled_since = '1.0.0'

@@ -913,2 +970,5 @@

.. versionadded:: 0.6.0
.. versionchanged:: 1.0.0
No longer produced, kept here for historic reasons.
This is covered with ``ruff`` formatter.

@@ -919,2 +979,3 @@ """

code = 319
disabled_since = '1.0.0'

@@ -949,2 +1010,5 @@

.. versionadded:: 0.6.0
.. versionchanged:: 1.0.0
No longer produced, kept here for historic reasons.
This is covered with ``ruff`` formatter.

@@ -955,2 +1019,3 @@ """

code = 320
disabled_since = '1.0.0'

@@ -988,4 +1053,4 @@

@final
class WrongMultilineStringViolation(TokenizeViolation):
'''
class UselessMultilineStringViolation(TokenizeViolation):
r'''
Forbid triple quotes for singleline strings.

@@ -1019,3 +1084,8 @@

.. versionadded:: 0.7.0
.. versionchanged:: 1.0.0
Now allows to have multiline strings without ``\n``
when that string is the single token on each line.
Also changed how multiline strings are detected.
'''

@@ -1069,2 +1139,5 @@

.. versionadded:: 0.14.0
.. versionchanged:: 1.0.0
No longer produced, kept here for historic reasons.
This is covered with ``ruff`` formatter. See ``UP031``.

@@ -1075,2 +1148,3 @@ """

code = 323
disabled_since = '1.0.0'

@@ -1189,2 +1263,5 @@

.. versionadded:: 0.7.0
.. versionchanged:: 1.0.0
No longer produced, kept here for historic reasons.
This is covered with ``ruff`` formatter. See ``ISC001``.

@@ -1195,2 +1272,3 @@ """

code = 326
disabled_since = '1.0.0'

@@ -1305,2 +1383,5 @@

.. versionadded:: 0.7.0
.. versionchanged:: 1.0.0
No longer produced, kept here for historic reasons.
This is covered with ``ruff`` linter. See ``TRY203``.

@@ -1311,2 +1392,3 @@ """

code = 329
disabled_since = '1.0.0'

@@ -1391,2 +1473,5 @@

.. versionchanged:: 0.14.0
.. versionchanged:: 1.0.0
No longer produced, kept here for historic reasons.
This is covered with ``ruff`` formatter. See ``RET504``.

@@ -1397,2 +1482,3 @@ """

code = 331
disabled_since = '1.0.0'

@@ -1403,3 +1489,3 @@

"""
Forbid walrus operator.
Forbid the use of the walrus operator (`:=`) outside of comprehensions.

@@ -1413,3 +1499,4 @@ Reasoning:

Solution:
Don't use fancy stuff, use good old assignments.
Avoid using the walrus operator outside comprehensions.
Stick to traditional assignment statements for clarity.

@@ -1428,6 +1515,8 @@ Example::

.. versionadded:: 0.14.0
.. versionchanged:: 1.0.0
Allows ``:=`` inside comprehensions.
"""
error_template = 'Found walrus operator'
error_template = 'Found walrus operator outside a comprehension'
code = 332

@@ -1460,2 +1549,5 @@

.. versionadded:: 0.10.0
.. versionchanged:: 1.0.0
No longer produced, kept here for historic reasons.
This is covered with ``ruff`` and ``pylint`` linters. See ``PLR1716``.

@@ -1466,2 +1558,3 @@ """

error_template = 'Found implicit complex compare'
disabled_since = '1.0.0'

@@ -1604,2 +1697,5 @@

.. versionchanged:: 0.11.0
.. versionchanged:: 1.0.0
No longer produced, kept here for historic reasons.
This is covered with ``ruff`` formatter.

@@ -1610,3 +1706,3 @@ """

code = 337
previous_codes = {465}
disabled_since = '1.0.0'

@@ -1705,2 +1801,5 @@

.. versionadded:: 0.12.0
.. versionchanged:: 1.0.0
No longer produced, kept here for historic reasons.
This is covered with ``ruff`` formatter.

@@ -1711,2 +1810,3 @@ """

code = 340
disabled_since = '1.0.0'

@@ -1735,2 +1835,5 @@

.. versionadded:: 0.12.0
.. versionchanged:: 1.0.0
No longer produced, kept here for historic reasons.
This is covered with ``ruff`` formatter.

@@ -1741,2 +1844,3 @@ """

code = 341
disabled_since = '1.0.0'

@@ -1795,2 +1899,5 @@

.. versionadded:: 0.12.0
.. versionchanged:: 1.0.0
No longer produced, kept here for historic reasons.
This is covered with ``ruff`` formatter.

@@ -1801,2 +1908,3 @@ """

code = 343
disabled_since = '1.0.0'

@@ -1988,2 +2096,5 @@

.. versionadded:: 0.13.0
.. versionchanged:: 1.0.0
No longer produced, kept here for historic reasons.
It conflicted with the ``ruff`` formatter.

@@ -1994,2 +2105,3 @@ """

code = 348
disabled_since = '1.0.0'

@@ -2069,2 +2181,5 @@

.. versionadded:: 0.13.0
.. versionchanged:: 1.0.0
No longer produced, kept here for historic reasons.
This is covered with ``ruff`` linter. See ``UP018`` and ``C408``.

@@ -2075,2 +2190,3 @@ """

code = 351
disabled_since = '1.0.0'

@@ -2104,2 +2220,5 @@

.. versionadded:: 0.13.0
.. versionchanged:: 1.0.0
No longer produced, kept here for historic reasons.
This is covered with ``ruff`` formatter.

@@ -2110,2 +2229,3 @@ """

code = 352
disabled_since = '1.0.0'

@@ -2198,2 +2318,5 @@

.. versionadded:: 0.13.0
.. versionchanged:: 1.0.0
No longer produced, kept here for historic reasons.
This is covered with ``ruff`` formatter.

@@ -2204,2 +2327,3 @@ """

code = 355
disabled_since = '1.0.0'

@@ -2225,2 +2349,3 @@

first, *iterable = other_iterable
GenericTuple = tuple[*Shape]

@@ -2232,2 +2357,5 @@ # Wrong:

.. versionadded:: 0.13.0
.. versionchanged:: 0.19.3
Allow using ``TypeVarTuple`` unpacking in generic types.
As a side-effect we now allow all unpackings in ``ast.Subscript``.

@@ -2342,2 +2470,5 @@ """

.. versionadded:: 0.15.0
.. versionchanged:: 1.0.0
No longer produced, kept here for historic reasons.
This is covered with ``ruff`` formatter.

@@ -2348,2 +2479,3 @@ """

code = 360
disabled_since = '1.0.0'

@@ -2382,2 +2514,5 @@

.. versionadded:: 0.15.0
.. versionchanged:: 1.0.0
No longer produced, kept here for historic reasons.
This is covered with ``ruff`` formatter.

@@ -2388,2 +2523,3 @@ """

code = 361
disabled_since = '1.0.0'

@@ -2428,1 +2564,28 @@

code = 362
@final
class RaiseSystemExitViolation(ASTViolation):
"""
Forbid raising :exc:`SystemExit`.
Reasoning:
For consistency.
Solution:
Use :func:`sys.exit`.
Example::
# Correct:
sys.exit(code)
# Wrong:
raise SystemExit(code)
.. versionadded:: 1.0.0
"""
error_template = 'Found `raise SystemExit`, instead of using `sys.exit`'
code = 363

@@ -178,3 +178,3 @@ """

from typing_extensions import final
from typing import final

@@ -293,2 +293,3 @@ from wemake_python_styleguide.violations.base import (

@final

@@ -309,6 +310,2 @@ class WrongVariableNameViolation(ASTViolation):

See
:py:data:`~wemake_python_styleguide.constants.VARIABLE_NAMES_BLACKLIST`
for the base list of blacklisted variable names.
Example::

@@ -375,2 +372,6 @@

Pass allowed short names with ``--allowed-domain-names``.
Default:
:str:`wemake_python_styleguide.options.defaults.ALLOWED_DOMAIN_NAMES`
.. versionadded:: 0.1.0

@@ -438,16 +439,8 @@ .. versionchanged:: 0.4.0

When `--i-control-code` is set to ``False``
you can reexport things with ``as``,
because ``mypy`` might require it
with ``implicit_reexport = False`` setting turned on.
Configuration:
This rule is configurable with ``--i-control-code``
and ``--i-dont-control-code``.
Default:
:str:`wemake_python_styleguide.options.defaults.I_CONTROL_CODE`
.. versionadded:: 0.1.0
.. versionchanged:: 0.13.0
.. versionchanged:: 0.14.0
.. versionchanged:: 1.0.0
No longer produced, kept here for historic reasons.
This is covered with ``ruff`` and ``pylint`` linters. See ``PLC0414``.

@@ -458,2 +451,3 @@ """

code = 113
disabled_since = '1.0.0'

@@ -654,2 +648,5 @@

.. versionadded:: 0.5.0
.. versionchanged:: 1.0.0
Only produced for filenames now.
Code is covered with ``ruff`` and ``pylint`` linter. See ``PLC2401``.

@@ -877,8 +874,7 @@ """

See
:py:data:`~wemake_python_styleguide.constants.BUILTINS_WHITELIST`
for full list of builtins we allow to shadow.
.. versionadded:: 0.14
.. versionchanged:: 0.15
.. versionchanged:: 1.0.0
No longer produced, kept here for historic reasons.
This is covered with ``ruff`` linter. See ``A001``.

@@ -889,1 +885,2 @@ """

code = 125
disabled_since = '1.0.0'

@@ -32,2 +32,3 @@ """

BuggySuperContextViolation
LambdaAttributeAssignedViolation

@@ -54,6 +55,7 @@ Respect your objects

.. autoclass:: BuggySuperContextViolation
.. autoclass:: LambdaAttributeAssignedViolation
"""
from typing_extensions import final
from typing import final

@@ -94,2 +96,4 @@ from wemake_python_styleguide.violations.base import ASTViolation

.. versionchanged:: 0.11.0
.. versionchanged:: 1.0.0
Allows subclassing builtins in ``enum.Enum`` definitions.

@@ -100,3 +104,2 @@ """

code = 600
previous_codes = {426}

@@ -149,2 +152,5 @@

.. versionchanged:: 0.14.0
.. versionchanged:: 1.0.0
Allow to shadow class attribute names in ``@dataclass`` classes.
.. _mypyc: https://github.com/python/mypy/tree/master/mypyc

@@ -156,3 +162,2 @@

code = 601
previous_codes = {427}

@@ -175,5 +180,2 @@

See also:
webucator.com/article/when-to-use-static-methods-in-python-never
"""

@@ -183,3 +185,2 @@

code = 602
previous_codes = {433}

@@ -215,3 +216,2 @@

code = 603
previous_codes = {434}

@@ -253,3 +253,2 @@

code = 604
previous_codes = {452}

@@ -290,3 +289,2 @@

code = 605
previous_codes = {453}

@@ -329,3 +327,2 @@

code = 606
previous_codes = {454}

@@ -378,3 +375,2 @@

code = 607
previous_codes = {455}

@@ -411,3 +407,2 @@

code = 608
previous_codes = {456}

@@ -442,10 +437,8 @@

See
:py:data:`~wemake_python_styleguide.constants.ALL_MAGIC_METHODS`
for the full list of magic attributes disallowed from being
accessed directly.
.. versionadded:: 0.8.0
.. versionchanged:: 0.11.0
.. versionchanged:: 0.16.0
.. versionchanged:: 1.0.0
No longer produced, kept here for historic reasons.
This is covered with ``ruff`` and ``pylint`` linters. See ``PLC2801``.

@@ -456,3 +449,3 @@ """

code = 609
previous_codes = {462}
disabled_since = '1.0.0'

@@ -535,2 +528,3 @@

https://docs.python.org/3/reference/datamodel.html
https://docs.astral.sh/ruff/rules/yield-in-init

@@ -545,3 +539,2 @@ .. versionadded:: 0.3.0

code = 611
previous_codes = {439, 435}

@@ -577,2 +570,6 @@

.. versionadded:: 0.12.0
.. versionchanged:: 1.0.0
Ignores cases when ``super().method`` is called
when function parameters have defaults.
Because defaults might be different.

@@ -714,9 +711,10 @@ """

# Correct
# Correct:
(super(cls, self).augment(it) for it in items)
# Wrong
# Wrong:
(super().augment(it) for it in items)
.. versionadded:: 0.18.0
"""

@@ -726,1 +724,35 @@

code = 616
@final
class LambdaAttributeAssignedViolation(ASTViolation):
"""
Forbid using ``lambda`` as an assigned attribute.
Reasoning:
Assigning ``lambda`` as an attribute does not make much sense.
And can lead to potentially incorrect code.
Solution:
Use ``def`` statements to create regular or class methods.
Example::
# Correct:
class Used:
def login(self): ...
# Wrong:
class User:
def __init__(self):
self.login = lambda: ...
See also:
https://docs.astral.sh/ruff/rules/lambda-assignment
.. versionadded:: 1.0.0
"""
error_template = 'Found lambda assigned as an attribute'
code = 617

@@ -49,2 +49,6 @@ """

ChainedIsViolation
DuplicateIfConditionViolation
UselessTernaryViolation
DuplicateCasePatternViolation
ExtraMatchSubjectSyntaxViolation

@@ -87,6 +91,10 @@ Refactoring opportunities

.. autoclass:: ChainedIsViolation
.. autoclass:: DuplicateIfConditionViolation
.. autoclass:: UselessTernaryViolation
.. autoclass:: DuplicateCasePatternViolation
.. autoclass:: ExtraMatchSubjectSyntaxViolation
"""
from typing_extensions import final
from typing import final

@@ -142,3 +150,2 @@ from wemake_python_styleguide.violations.base import (

code = 500
previous_codes = {436}

@@ -184,3 +191,2 @@

code = 501
previous_codes = {437}

@@ -216,2 +222,5 @@

.. versionchanged:: 0.11.0
.. versionchanged:: 1.0.0
No longer produced, kept here for historic reasons.
This is covered with ``ruff`` linter. See ``SIM108`` and ``SIM210``.

@@ -222,3 +231,3 @@ """

code = 502
previous_codes = {451}
disabled_since = '1.0.0'

@@ -263,2 +272,5 @@

.. versionchanged:: 0.15.1
.. versionchanged:: 1.0.0
No longer produced, kept here for historic reasons.
This is covered with ``ruff`` linter. See ``RET505``.

@@ -269,3 +281,3 @@ """

code = 503
previous_codes = {457}
disabled_since = '1.0.0'

@@ -289,8 +301,8 @@

if some == 1:
...
...
else:
...
...
if not some:
...
...

@@ -304,9 +316,13 @@ if not some:

if not some:
...
...
else:
...
...
.. versionadded:: 0.8.0
.. versionchanged:: 0.11.0
.. versionchanged:: 1.0.0
We now also detect ``is not`` and ``not in`` as negated conditions.
We now allow using all negated conditions in ``if/elif/else`` cases.
"""

@@ -316,3 +332,2 @@

code = 504
previous_codes = {463}

@@ -367,3 +382,2 @@

code = 505
previous_codes = {464}

@@ -400,3 +414,2 @@

code = 506
previous_codes = {467}

@@ -432,2 +445,5 @@

.. versionchanged:: 0.11.0
.. versionchanged:: 1.0.0
No longer produced, kept here for historic reasons.
This is covered with ``ruff`` and ``pylint`` linters. See ``PLC1802``.

@@ -438,3 +454,3 @@ """

code = 507
previous_codes = {468}
disabled_since = '1.0.0'

@@ -466,2 +482,5 @@

.. versionchanged:: 0.11.0
.. versionchanged:: 1.0.0
No longer produced, kept here for historic reasons.
This is covered with ``ruff`` linter. See ``SIM201``.

@@ -472,3 +491,3 @@ """

code = 508
previous_codes = {470}
disabled_since = '1.0.0'

@@ -511,3 +530,2 @@

code = 509
previous_codes = {472}

@@ -546,2 +564,5 @@

.. versionchanged:: 0.14.0
.. versionchanged:: 1.0.0
No longer produced, kept here for historic reasons.
This is covered with ``ruff`` and ``pylint`` linters. See ``PLR6201``.

@@ -552,3 +573,3 @@ """

code = 510
previous_codes = {473}
disabled_since = '1.0.0'

@@ -572,3 +593,3 @@

# Correct:
isinstance(some, (int, float))
isinstance(some, int | float)

@@ -583,2 +604,5 @@ # Wrong:

.. versionchanged:: 0.11.0
.. versionchanged:: 1.0.0
No longer produced, kept here for historic reasons.
This is covered with ``ruff`` formatter. See ``SIM101``.

@@ -591,3 +615,3 @@ """

code = 511
previous_codes = {474}
disabled_since = '1.0.0'

@@ -610,3 +634,3 @@

# Correct:
isinstance(some, (int, float))
isinstance(some, int | float)
isinstance(some, int)

@@ -621,2 +645,5 @@

.. versionchanged:: 0.11.0
.. versionchanged:: 1.0.0
No longer produced, kept here for historic reasons.
This is covered with ``ruff`` linter. See ``UP038``.

@@ -627,3 +654,3 @@ """

code = 512
previous_codes = {475}
disabled_since = '1.0.0'

@@ -680,3 +707,2 @@

Example::

@@ -694,2 +720,5 @@

.. versionchanged:: 0.12.0
.. versionchanged:: 1.0.0
No longer produced, kept here for historic reasons.
This is covered with ``ruff`` and ``pylint`` linters. See ``PLR1714``.

@@ -700,3 +729,3 @@ """

error_template = 'Found implicit `in` condition: {0}'
previous_codes = {336}
disabled_since = '1.0.0'

@@ -791,2 +820,5 @@

See also:
https://docs.astral.sh/ruff/rules/unnecessary-dict-kwargs
.. versionadded:: 0.12.0

@@ -880,2 +912,3 @@

We disallow complex constants like tuple, dicts, and lists.
We also allow to compare any values inside ``assert`` statements.

@@ -907,2 +940,4 @@ Reasoning:

.. versionadded:: 0.12.0
.. versionchanged:: 1.0.0
Allows any compares in ``assert`` statements.

@@ -945,2 +980,5 @@ """

.. versionadded:: 0.12.0
.. versionchanged:: 1.0.0
No longer produced, kept here for historic reasons.
This is covered with ``ruff`` linter. See ``F632``.

@@ -951,2 +989,3 @@ """

error_template = 'Found wrong `is` compare'
disabled_since = '1.0.0'

@@ -1072,2 +1111,5 @@

.. versionadded:: 0.13.0
.. versionchanged:: 1.0.0
No longer produced, kept here for historic reasons.
This is covered with ``ruff`` linter. See ``FURB171``.

@@ -1078,2 +1120,3 @@ """

code = 525
disabled_since = '1.0.0'

@@ -1111,2 +1154,5 @@

.. versionadded:: 0.13.0
.. versionchanged:: 1.0.0
No longer produced, kept here for historic reasons.
This is covered with ``ruff`` linter. See ``UP028``.

@@ -1117,2 +1163,3 @@ """

code = 526
disabled_since = '1.0.0'

@@ -1177,2 +1224,5 @@

.. versionadded:: 0.13.0
.. versionchanged:: 1.0.0
No longer produced, kept here for historic reasons.
This is covered with ``ruff`` and ``pylint`` linters. See ``PLC0206``.

@@ -1183,2 +1233,3 @@ """

code = 528
disabled_since = '1.0.0'

@@ -1276,2 +1327,5 @@

.. versionadded:: 0.15.0
.. versionchanged:: 1.0.0
No longer produced, kept here for historic reasons.
This is covered with ``ruff`` linter. See ``RET505``.

@@ -1282,2 +1336,3 @@ """

code = 531
disabled_since = '1.0.0'

@@ -1311,1 +1366,129 @@

code = 532
@final
class DuplicateIfConditionViolation(ASTViolation):
"""
Forbid having duplicate conditions in several ``if`` // ``elif`` branches.
Reasoning:
It is likely an error to have multiple same condition
in ``if`` / ``elif`` statements. Only the first one will always work.
Solution:
Change the condition.
Example::
# Correct:
if something:
...
elif other:
...
# Wrong:
if something:
...
elif something:
...
.. versionadded:: 1.0.0
"""
error_template = 'Found duplicate condition in `if`: {0}'
code = 533
@final
class UselessTernaryViolation(ASTViolation):
"""
Forbid having useless ternary expressions.
Reasoning:
When ternary expression can be replaced with a single name,
it is way more readable and more performant.
Solution:
Remove the ternary expression.
Example::
# Correct:
first if some_condition else second
# Wrong:
a if a is not None else None
b if a == b else a
.. versionadded:: 1.0.0
"""
error_template = 'Found useless ternary expression'
code = 534
@final
class DuplicateCasePatternViolation(ASTViolation):
"""
Forbid having duplicate ``case`` patterns.
Reasoning:
It is likely an error to have multiple same int ``case`` patterns.
Only the first one will always work.
Solution:
Change the pattern.
Example::
# Correct:
match some:
case SomeClass(field) if field > 0: ...
case OtherClass(): ...
# Wrong:
match some:
case SomeClass(field): ...
case SomeClass(field): ...
.. versionadded:: 1.0.0
"""
error_template = 'Found duplicate `case` pattern: {0}'
code = 535
@final
class ExtraMatchSubjectSyntaxViolation(ASTViolation):
"""
Forbid extra syntax around ``match`` like ``[]`` or ``{ ... }``.
Reasoning:
Adding extra lists / sets / dicts around your ``match`` subjects
is just adding more complexity.
Solution:
Use raw values or tuples instead.
Example::
# Correct:
match some:
case SomeClass(): ...
match (first, second):
case (1, 2): ...
# Wrong:
match [first, second]:
case [1, 2]: ...
.. versionadded:: 1.0.0
"""
error_template = 'Found `match` subject with extra syntax: {0}'
code = 536

@@ -25,3 +25,3 @@ """

from typing_extensions import final
from typing import final

@@ -28,0 +28,0 @@ from wemake_python_styleguide.violations.base import SimpleViolation

import ast
from collections import defaultdict
from typing import Callable, DefaultDict, List, Set, Tuple, Union, cast
from typing import TypeAlias, cast, final
from typing_extensions import TypeAlias, final
from wemake_python_styleguide.compat.aliases import ForNodes, WithNodes
from wemake_python_styleguide.compat.types import NamedMatch
from wemake_python_styleguide.logic.naming.name_nodes import flat_variable_names
from wemake_python_styleguide.logic import walk
from wemake_python_styleguide.logic.naming import name_nodes
from wemake_python_styleguide.logic.nodes import get_context, get_parent
from wemake_python_styleguide.logic.scopes import defs, predicates
from wemake_python_styleguide.logic.walk import is_contained_by
from wemake_python_styleguide.types import (
AnyAssignWithWalrus,
AnyFor,
AnyFunctionDef,
AnyImport,
AnyWith,
)
from wemake_python_styleguide.violations.best_practices import (
BlockAndLocalOverlapViolation,
ControlVarUsedAfterBlockViolation,
OuterScopeShadowingViolation,
)

@@ -28,161 +19,16 @@ from wemake_python_styleguide.visitors import base, decorators

#: That's how we represent contexts for control variables.
_BlockVariables: TypeAlias = DefaultDict[
_BlockVariables: TypeAlias = defaultdict[
ast.AST,
DefaultDict[str, List[ast.AST]],
defaultdict[str, list[ast.AST]],
]
#: That's how we filter some overlaps that do happen in Python:
_ScopePredicate: TypeAlias = Callable[[ast.AST, Set[str]], bool]
_NamePredicate: TypeAlias = Callable[[ast.AST], bool]
#: Named nodes.
_NamedNode: TypeAlias = Union[
AnyFunctionDef,
ast.ClassDef,
ast.ExceptHandler,
NamedMatch,
]
@final
@decorators.alias('visit_named_nodes', (
'visit_FunctionDef',
'visit_AsyncFunctionDef',
'visit_ClassDef',
'visit_ExceptHandler',
'visit_MatchAs',
'visit_MatchStar',
))
@decorators.alias('visit_any_for', (
'visit_For',
'visit_AsyncFor',
))
@decorators.alias('visit_locals', (
'visit_Assign',
'visit_AnnAssign',
'visit_NamedExpr',
'visit_arg',
))
class BlockVariableVisitor(base.BaseNodeVisitor):
"""
This visitor is used to detect variables that are reused for blocks.
Check out this example:
.. code::
exc = 7
try:
...
except Exception as exc: # reusing existing variable
...
Please, do not modify. This is fragile and complex.
"""
_naming_predicates: Tuple[_NamePredicate, ...] = (
predicates.is_property_setter,
predicates.is_function_overload,
predicates.is_no_value_annotation,
)
_scope_predicates: Tuple[_ScopePredicate, ...] = (
lambda node, names: predicates.is_property_setter(node),
predicates.is_same_value_reuse,
predicates.is_same_try_except_cases,
)
# Blocks:
def visit_named_nodes(self, node: _NamedNode) -> None:
"""Visits block nodes that have ``.name`` property."""
names = {node.name} if node.name else set()
self._scope(node, names, is_local=False)
self._outer_scope(node, names)
self.generic_visit(node)
def visit_any_for(self, node: AnyFor) -> None:
"""Collects block nodes from loop definitions."""
names = defs.extract_names(node.target)
self._scope(node, names, is_local=False)
self._outer_scope(node, names)
self.generic_visit(node)
def visit_alias(self, node: ast.alias) -> None:
"""Aliases from ``import`` and ``from ... import`` block nodes."""
parent = cast(AnyImport, get_parent(node))
import_name = {node.asname} if node.asname else {node.name}
self._scope(parent, import_name, is_local=False)
self._outer_scope(parent, import_name)
self.generic_visit(node)
def visit_withitem(self, node: ast.withitem) -> None:
"""Visits ``with`` and ``async with`` declarations."""
if node.optional_vars:
parent = cast(AnyWith, get_parent(node))
names = defs.extract_names(node.optional_vars)
self._scope(parent, names, is_local=False)
self._outer_scope(parent, names)
self.generic_visit(node)
# Locals:
def visit_locals(self, node: Union[AnyAssignWithWalrus, ast.arg]) -> None:
"""Visits local variable definitions and function arguments."""
if isinstance(node, ast.arg):
names = {node.arg}
else:
names = set(flat_variable_names([node]))
self._scope(node, names, is_local=True)
self._outer_scope(node, names)
self.generic_visit(node)
# Utils:
def _scope(
self,
node: ast.AST,
names: Set[str],
*,
is_local: bool,
) -> None:
scope = defs.BlockScope(node)
shadow = scope.shadowing(names, is_local=is_local)
ignored_scope = any(
predicate(node, names)
for predicate in self._scope_predicates
)
ignored_name = any(
predicate(node)
for predicate in self._naming_predicates
)
if shadow and not ignored_scope:
self.add_violation(
BlockAndLocalOverlapViolation(node, text=', '.join(shadow)),
)
if not ignored_name:
scope.add_to_scope(names, is_local=is_local)
def _outer_scope(self, node: ast.AST, names: Set[str]) -> None:
scope = defs.OuterScope(node)
shadow = scope.shadowing(names)
if shadow:
self.add_violation(
OuterScopeShadowingViolation(node, text=', '.join(shadow)),
)
scope.add_to_scope(names)
@final
@decorators.alias('visit_any_for', (
'visit_For',
'visit_AsyncFor',
))
@decorators.alias(
'visit_any_for',
(
'visit_For',
'visit_AsyncFor',
),
)
class AfterBlockVariablesVisitor(base.BaseNodeVisitor):

@@ -202,3 +48,6 @@ """Visitor that ensures that block variables are not used after block."""

"""Visit loops."""
self._add_to_scope(node, defs.extract_names(node.target))
self._add_to_scope(
node,
set(name_nodes.get_variables_from_node(node.target)),
)
self.generic_visit(node)

@@ -211,3 +60,3 @@

cast(AnyWith, get_parent(node)),
defs.extract_names(node.optional_vars),
set(name_nodes.get_variables_from_node(node.optional_vars)),
)

@@ -226,3 +75,3 @@ self.generic_visit(node)

def _add_to_scope(self, node: ast.AST, names: Set[str]) -> None:
def _add_to_scope(self, node: ast.AST, names: set[str]) -> None:
context = cast(ast.AST, get_context(node))

@@ -233,6 +82,9 @@ for var_name in names:

def _check_variable_usage(self, node: ast.Name) -> None:
if walk.get_closest_parent(node, ast.Assert):
return # Allow any names to be used in `assert` statements
context = cast(ast.AST, get_context(node))
blocks = self._block_variables[context][node.id]
is_contained_block_var = any(
is_contained_by(node, block) for block in blocks
walk.is_contained_by(node, block) for block in blocks
)

@@ -243,5 +95,3 @@ # Restrict the use of block variables with the same name to

isinstance(block, ForNodes) for block in blocks
) or all(
isinstance(block, WithNodes) for block in blocks
)
) or all(isinstance(block, WithNodes) for block in blocks)
# Return if not a block variable or a contained block variable.

@@ -248,0 +98,0 @@ if not blocks or (is_contained_block_var and is_same_type_block):

import ast
import re
import string
from collections import Counter, defaultdict
from collections.abc import Hashable
from contextlib import suppress
from typing import (
ClassVar,
DefaultDict,
FrozenSet,
List,
Optional,
Pattern,
Sequence,
Union,
)
from collections.abc import Sequence
from typing import ClassVar, Final, TypeAlias, final
from typing_extensions import Final, TypeAlias, final
from wemake_python_styleguide import constants

@@ -24,10 +10,7 @@ from wemake_python_styleguide.compat.aliases import (

FunctionNodes,
TextNodes,
)
from wemake_python_styleguide.logic import nodes, safe_eval, source, walk
from wemake_python_styleguide.logic import nodes, source, walk
from wemake_python_styleguide.logic.tree import (
attributes,
functions,
operators,
strings,
variables,

@@ -39,3 +22,2 @@ )

AnyNodes,
AnyText,
AnyWith,

@@ -51,58 +33,34 @@ )

#: Items that can be inside a hash.
_HashItems: TypeAlias = Sequence[Optional[ast.AST]]
_HashItems: TypeAlias = Sequence[ast.AST | None]
@final
@decorators.alias('visit_any_string', (
'visit_Str',
'visit_Bytes',
))
@decorators.alias(
'visit_any_string',
(
'visit_Str',
'visit_Bytes',
),
)
class WrongStringVisitor(base.BaseNodeVisitor):
"""Restricts several string usages."""
_string_constants: ClassVar[FrozenSet[str]] = frozenset((
string.ascii_letters,
string.ascii_lowercase,
string.ascii_uppercase,
string.digits,
string.octdigits,
string.hexdigits,
string.printable,
string.whitespace,
string.punctuation,
))
#: Copied from https://stackoverflow.com/a/30018957/4842742
_modulo_string_pattern: ClassVar[Pattern[str]] = re.compile(
r""" # noqa: WPS323
( # start of capture group 1
% # literal "%"
(?: # first option
(?:\([a-zA-Z][\w_]*\))? # optional named group
(?:[#0+-]{0,5}) # optional flags (except " ")
(?:\d+|\*)? # width
(?:\.(?:\d+|\*))? # precision
(?:h|l|L)? # size
[diouxXeEfFgGcrsa] # type
) | %% # OR literal "%%"
) # end
""", # noqa: WPS323
# Different python versions report `WPS323` on different lines.
flags=re.X, # flag to ignore comments and whitespace.
_string_constants: ClassVar[frozenset[str]] = frozenset(
(
string.ascii_letters,
string.ascii_lowercase,
string.ascii_uppercase,
string.digits,
string.octdigits,
string.hexdigits,
string.printable,
string.whitespace,
string.punctuation,
),
)
#: Names of functions in which we allow strings with modulo patterns.
_modulo_pattern_exceptions: ClassVar[FrozenSet[str]] = frozenset((
'strftime', # For date, time, and datetime.strftime()
'strptime', # For date, time, and datetime.strptime()
'execute', # For psycopg2's cur.execute()
))
def visit_any_string(self, node: AnyText) -> None:
def visit_any_string(self, node: ast.Constant) -> None:
"""Forbids incorrect usage of strings."""
text_data = source.render_string(node.s)
text_data = source.render_string(node.value)
self._check_is_alphabet(node, text_data)
self._check_modulo_patterns(node, text_data)
self.generic_visit(node)

@@ -112,4 +70,4 @@

self,
node: AnyText,
text_data: Optional[str],
node: ast.Constant,
text_data: str | None,
) -> None:

@@ -119,38 +77,8 @@ if text_data in self._string_constants:

best_practices.StringConstantRedefinedViolation(
node, text=text_data,
node,
text=text_data,
),
)
def _is_modulo_pattern_exception(self, parent: Optional[ast.AST]) -> bool:
"""
Check if string with modulo pattern is in an exceptional situation.
Basically we have some function names in which we allow strings with
modulo patterns because they must have them for the functions to work
properly.
"""
if parent and isinstance(parent, ast.Call):
return bool(functions.given_function_called(
parent,
self._modulo_pattern_exceptions,
split_modules=True,
))
return False
def _check_modulo_patterns(
self,
node: AnyText,
text_data: Optional[str],
) -> None:
parent = nodes.get_parent(node)
if parent and strings.is_doc_string(parent):
return # we allow `%s` in docstrings: they cannot be formatted.
if text_data and self._modulo_string_pattern.search(text_data):
if not self._is_modulo_pattern_exception(parent):
self.add_violation(
consistency.ModuloStringFormatViolation(node),
)
@final

@@ -161,6 +89,4 @@ class WrongFormatStringVisitor(base.BaseNodeVisitor):

_valid_format_index: ClassVar[AnyNodes] = (
*TextNodes,
ast.Num,
ast.Constant,
ast.Name,
ast.NameConstant,
)

@@ -181,30 +107,13 @@ _single_use_types: ClassVar[AnyNodes] = (

if not isinstance(nodes.get_parent(node), ast.FormattedValue):
# We don't allow `f` strings by default,
# But, we need this condition to make sure that this
# We need this condition to make sure that this
# is not a part of complex string format like `f"Count={count:,}"`:
self._check_complex_formatted_string(node)
self.add_violation(consistency.FormattedStringViolation(node))
self.generic_visit(node)
def _check_complex_formatted_string(self, node: ast.JoinedStr) -> None:
"""
Whitelists all simple uses of f strings.
Checks if list, dict, function call with no parameters or variable.
"""
has_formatted_components = any(
isinstance(comp, ast.FormattedValue)
for comp in node.values
)
if not has_formatted_components:
self.add_violation( # If no formatted values
complexity.TooComplexFormattedStringViolation(node),
)
return
"""Allows all simple uses of `f` strings."""
for string_component in node.values:
if isinstance(string_component, ast.FormattedValue):
# Test if possible chaining is invalid
format_value = string_component.value
if self._is_valid_formatted_value(format_value):
if self._is_valid_formatted_value(string_component.value):
continue

@@ -214,8 +123,10 @@ self.add_violation( # Everything else is too complex:

)
break
return
def _is_valid_formatted_value(self, format_value: ast.AST) -> bool:
if isinstance(format_value, self._chainable_types):
if not self._is_valid_chaining(format_value):
return False
if isinstance(
format_value,
self._chainable_types,
) and not self._is_valid_chaining(format_value):
return False
return self._is_valid_final_value(format_value)

@@ -225,9 +136,8 @@

# Variable lookup is okay and a single attribute is okay
if isinstance(format_value, (ast.Name, ast.Attribute)):
if isinstance(format_value, ast.Name | ast.Attribute) or (
isinstance(format_value, ast.Call) and len(format_value.args) <= 3
):
return True
# Function call with empty arguments is okay
elif isinstance(format_value, ast.Call) and not format_value.args:
return True
# Named lookup, Index lookup & Dict key is okay
elif isinstance(format_value, ast.Subscript):
if isinstance(format_value, ast.Subscript):
return isinstance(

@@ -240,3 +150,3 @@ format_value.slice,

def _is_valid_chaining(self, format_value: AnyChainable) -> bool:
chained_parts: List[ast.AST] = list(attributes.parts(format_value))
chained_parts: list[ast.AST] = list(attributes.parts(format_value))
if len(chained_parts) <= self._max_chained_items:

@@ -246,7 +156,6 @@ return self._is_valid_chain_structure(chained_parts)

def _is_valid_chain_structure(self, chained_parts: List[ast.AST]) -> bool:
def _is_valid_chain_structure(self, chained_parts: list[ast.AST]) -> bool:
"""Helper method for ``_is_valid_chaining``."""
has_invalid_parts = any(
not self._is_valid_final_value(part)
for part in chained_parts
not self._is_valid_final_value(part) for part in chained_parts
)

@@ -258,6 +167,9 @@ if has_invalid_parts:

# call. This is because we don't allow name.attr.attr
return sum(
isinstance(part, self._single_use_types)
for part in chained_parts
) == 1
return (
sum(
isinstance(part, self._single_use_types)
for part in chained_parts
)
== 1
)
return True # All chaining with fewer elements is fine!

@@ -272,7 +184,5 @@

*AssignNodesWithWalrus,
# Constructor usages:
*FunctionNodes,
ast.arguments,
# Primitives:

@@ -287,3 +197,3 @@ ast.List,

def visit_Num(self, node: ast.Num) -> None:
def visit_Num(self, node: ast.Constant) -> None:
"""Checks wrong constants inside the code."""

@@ -294,3 +204,3 @@ self._check_is_magic(node)

def _check_is_magic(self, node: ast.Num) -> None:
def _check_is_magic(self, node: ast.Constant) -> None:
parent = operators.get_parent_ignoring_unary(node)

@@ -300,15 +210,15 @@ if isinstance(parent, self._allowed_parents):

if node.n in constants.MAGIC_NUMBERS_WHITELIST:
if node.value in constants.MAGIC_NUMBERS_WHITELIST:
return
if isinstance(node.n, int) and node.n <= self._non_magic_modulo:
if isinstance(node.value, int) and node.value <= self._non_magic_modulo:
return
self.add_violation(
best_practices.MagicNumberViolation(node, text=str(node.n)),
best_practices.MagicNumberViolation(node, text=str(node.value)),
)
def _check_is_approximate_constant(self, node: ast.Num) -> None:
def _check_is_approximate_constant(self, node: ast.Constant) -> None:
try:
precision = len(str(node.n).split('.')[1])
precision = len(str(node.value).split('.')[1])
except IndexError:

@@ -321,6 +231,7 @@ precision = 0

for constant in constants.MATH_APPROXIMATE_CONSTANTS:
if str(constant).startswith(str(node.n)):
if str(constant).startswith(str(node.value)):
self.add_violation(
best_practices.ApproximateConstantViolation(
node, text=str(node.n),
node,
text=str(node.value),
),

@@ -331,10 +242,16 @@ )

@final
@decorators.alias('visit_any_for', (
'visit_For',
'visit_AsyncFor',
))
@decorators.alias('visit_any_with', (
'visit_With',
'visit_AsyncWith',
))
@decorators.alias(
'visit_any_for',
(
'visit_For',
'visit_AsyncFor',
),
)
@decorators.alias(
'visit_any_with',
(
'visit_With',
'visit_AsyncWith',
),
)
class WrongAssignmentVisitor(base.BaseNodeVisitor):

@@ -349,3 +266,4 @@ """Visits all assign nodes."""

self._check_unpacking_targets(
node, withitem.optional_vars.elts,
node,
withitem.optional_vars.elts,
)

@@ -380,3 +298,3 @@ self.generic_visit(node)

if isinstance(node.targets[0], (ast.Tuple, ast.List)):
if isinstance(node.targets[0], ast.Tuple | ast.List):
self._check_unpacking_targets(node, node.targets[0].elts)

@@ -394,3 +312,3 @@ self.generic_visit(node)

node: ast.AST,
targets: List[ast.expr],
targets: list[ast.expr],
) -> None:

@@ -414,3 +332,3 @@ if len(targets) == 1:

def _check_unpacking_target_types(self, node: Optional[ast.AST]) -> None:
def _check_unpacking_target_types(self, node: ast.AST | None) -> None:
if not node:

@@ -428,9 +346,2 @@ return

_elements_in_sets: ClassVar[AnyNodes] = (
*TextNodes,
ast.Num,
ast.NameConstant,
ast.Name,
)
_unhashable_types: ClassVar[AnyNodes] = (

@@ -446,21 +357,4 @@ ast.List,

_elements_to_eval: ClassVar[AnyNodes] = (
*TextNodes,
ast.Num,
ast.NameConstant,
ast.Tuple,
ast.List,
ast.Set,
ast.Dict,
# Since python3.8 `BinOp` only works for complex numbers:
# https://github.com/python/cpython/pull/4035/files
# https://bugs.python.org/issue31778
ast.BinOp,
# Only our custom `eval` function can eval names safely:
ast.Name,
)
def visit_Set(self, node: ast.Set) -> None:
"""Ensures that set literals do not have any duplicate items."""
self._check_set_elements(node, node.elts)
self._check_unhashable_elements(node.elts)

@@ -471,3 +365,2 @@ self.generic_visit(node)

"""Ensures that dict literals do not have any duplicate keys."""
self._check_set_elements(node, node.keys)
self._check_unhashable_elements(node.keys)

@@ -482,13 +375,7 @@ self._check_float_keys(node.keys)

evaluates_to_float = False
if isinstance(dict_key, ast.BinOp):
evaluated_key = getattr(dict_key, 'wps_op_eval', None)
evaluates_to_float = isinstance(evaluated_key, float)
real_key = operators.unwrap_unary_node(dict_key)
is_float_key = (
isinstance(real_key, ast.Num) and
isinstance(real_key.n, float)
)
if is_float_key or evaluates_to_float:
if isinstance(real_key, ast.Constant) and isinstance(
real_key.value,
float,
):
self.add_violation(best_practices.FloatKeyViolation(dict_key))

@@ -505,65 +392,1 @@

)
def _check_set_elements(
self,
node: Union[ast.Set, ast.Dict],
keys_or_elts: _HashItems,
) -> None:
elements: List[str] = []
element_values = []
for set_item in keys_or_elts:
if set_item is None:
continue # happens for `{**a}`
real_item = operators.unwrap_unary_node(set_item)
if isinstance(real_item, self._elements_in_sets):
# Similar look:
node_repr = source.node_to_string(set_item)
elements.append(node_repr.strip().strip('(').strip(')'))
real_item = operators.unwrap_starred_node(real_item)
# Non-constant nodes raise ValueError,
# unhashables raise TypeError:
with suppress(ValueError, TypeError):
# Similar value:
element_values.append(
safe_eval.literal_eval_with_names(
real_item,
) if isinstance(
real_item, self._elements_to_eval,
) else set_item,
)
self._report_set_elements(node, elements, element_values)
def _report_set_elements(
self,
node: Union[ast.Set, ast.Dict],
elements: List[str],
element_values,
) -> None:
for look_element, look_count in Counter(elements).items():
if look_count > 1:
self.add_violation(
best_practices.NonUniqueItemsInHashViolation(
node, text=look_element,
),
)
return
value_counts: DefaultDict[Hashable, int] = defaultdict(int)
for value_element in element_values:
real_value = value_element if isinstance(
# Lists, sets, and dicts are not hashable:
value_element, Hashable,
) else str(value_element)
value_counts[real_value] += 1
if value_counts[real_value] > 1:
self.add_violation(
best_practices.NonUniqueItemsInHashViolation(
node, text=value_element,
),
)
import ast
from collections import defaultdict
from typing import ClassVar, DefaultDict, FrozenSet, List, Optional
from typing import ClassVar, final
from typing_extensions import final
from wemake_python_styleguide import constants, types

@@ -12,3 +10,3 @@ from wemake_python_styleguide.compat.aliases import AssignNodes, FunctionNodes

from wemake_python_styleguide.logic.arguments import function_args, super_args
from wemake_python_styleguide.logic.naming import access, name_nodes
from wemake_python_styleguide.logic.naming import access, enums, name_nodes
from wemake_python_styleguide.logic.tree import (

@@ -36,3 +34,2 @@ attributes,

"""Checking class definitions."""
self._check_base_classes_count(node)
self._check_base_classes(node)

@@ -42,16 +39,2 @@ self._check_kwargs_unpacking(node)

def _check_base_classes_count(self, node: ast.ClassDef) -> None:
is_object_explicit_base = (
len(node.bases) == 1 and
isinstance(node.bases[0], ast.Name) and
node.bases[0].id == 'object'
)
if is_object_explicit_base:
self.add_violation(
consistency.ExplicitObjectBaseClassViolation(
node,
text=node.name,
),
)
def _check_base_classes(self, node: ast.ClassDef) -> None:

@@ -68,14 +51,17 @@ for base_name in node.bases:

return True
elif isinstance(base_class, ast.Attribute):
if isinstance(base_class, ast.Attribute):
return all(
isinstance(sub_node, (ast.Name, ast.Attribute))
isinstance(sub_node, ast.Name | ast.Attribute)
for sub_node in attributes.parts(base_class)
)
elif isinstance(base_class, ast.Subscript):
if isinstance(base_class, ast.Subscript):
parts = list(attributes.parts(base_class))
subscripts = list(filter(
lambda part: isinstance(part, ast.Subscript), parts,
))
subscripts = list(
filter(
lambda part: isinstance(part, ast.Subscript),
parts,
),
)
correct_items = all(
isinstance(sub_node, (ast.Name, ast.Attribute, ast.Subscript))
isinstance(sub_node, ast.Name | ast.Attribute | ast.Subscript)
for sub_node in parts

@@ -96,10 +82,6 @@ )

self.add_violation(bp.BaseExceptionSubclassViolation(node))
elif id_attr == 'object' and len(node.bases) >= 2:
elif classes.is_forbidden_super_class(
id_attr,
) and not enums.has_enum_base(node):
self.add_violation(
consistency.ObjectInBaseClassesListViolation(
node, text=id_attr,
),
)
elif classes.is_forbidden_super_class(id_attr):
self.add_violation(
oop.BuiltinSubclassViolation(node, text=id_attr),

@@ -145,10 +127,9 @@ )

def _check_getters_setters_methods(self, node: ast.ClassDef) -> None:
getters_and_setters = set(filter(
lambda getter_setter: functions.is_method(
getattr(getter_setter, 'function_type', 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),
),
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:

@@ -164,15 +145,14 @@ self.add_violation(

@final
@decorators.alias('visit_any_function', (
'visit_FunctionDef',
'visit_AsyncFunctionDef',
))
@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__',
))
_staticmethod_names: ClassVar[frozenset[str]] = frozenset(('staticmethod',))
_special_async_iter: ClassVar[frozenset[str]] = frozenset(('__aiter__',))

@@ -214,7 +194,9 @@ def visit_any_function(self, node: types.AnyFunctionDef) -> None:

if node.name in constants.YIELD_MAGIC_METHODS_BLACKLIST:
if walk.is_contained(node, (ast.Yield, ast.YieldFrom)):
self.add_violation(
oop.YieldMagicMethodViolation(node, text=node.name),
)
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),
)

@@ -244,2 +226,8 @@ def _check_async_magic_methods(self, node: types.AnyFunctionDef) -> None:

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)

@@ -254,11 +242,11 @@ if call_stmt is None or not isinstance(call_stmt.func, ast.Attribute):

if not super_args.is_ordinary_super_call(attribute.value, class_name):
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
if not function_args.is_call_matched_by_arguments(node, call_stmt):
return
self.add_violation(
oop.UselessOverwrittenMethodViolation(
node, text=defined_method_name,
node,
text=defined_method_name,
),

@@ -270,3 +258,3 @@ )

node: types.AnyFunctionDef,
) -> Optional[ast.Call]:
) -> ast.Call | None:
"""

@@ -289,5 +277,4 @@ Fetches ``super`` call statement from function definition.

if statements_number == 2:
if not strings.is_doc_string(node.body[0]):
return None
if statements_number == 2 and not strings.is_doc_string(node.body[0]):
return None

@@ -298,3 +285,3 @@ stmt = node.body[-1]

return call_stmt if isinstance(call_stmt, ast.Call) else None
elif isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Call):
if isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Call):
return stmt.value

@@ -305,6 +292,9 @@ return None

@final
@decorators.alias('visit_any_assign', (
'visit_Assign',
'visit_AnnAssign',
))
@decorators.alias(
'visit_any_assign',
(
'visit_Assign',
'visit_AnnAssign',
),
)
class WrongSlotsVisitor(base.BaseNodeVisitor):

@@ -337,3 +327,3 @@ """Visits class attributes."""

) -> None:
fields: DefaultDict[str, List[ast.AST]] = defaultdict(list)
fields: defaultdict[str, list[ast.AST]] = defaultdict(list)

@@ -366,5 +356,5 @@ for tuple_item in elements.elts:

def _slot_item_name(self, node: ast.AST) -> Optional[str]:
if isinstance(node, ast.Str):
return node.s
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):

@@ -374,7 +364,7 @@ return source.node_to_string(node)

def _are_correct_slots(self, slots: List[ast.AST]) -> bool:
def _are_correct_slots(self, slots: list[ast.AST]) -> bool:
return all(
slot.s.isidentifier()
slot.value.isidentifier()
for slot in slots
if isinstance(slot, ast.Str)
if isinstance(slot, ast.Constant) and isinstance(slot.value, str)
)

@@ -385,10 +375,20 @@

class ClassAttributeVisitor(base.BaseNodeVisitor):
"""Finds incorrect class attributes."""
"""Finds incorrectattributes."""
def visit_ClassDef(self, node: ast.ClassDef) -> None:
"""Checks that class attributes are correct."""
"""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(

@@ -411,3 +411,21 @@ node,

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

@@ -423,11 +441,17 @@ class ClassMethodOrderVisitor(base.BaseNodeVisitor):

def _check_method_order(self, node: ast.ClassDef) -> None:
method_nodes: List[str] = []
method_nodes = [
subnode.name
for subnode in ast.walk(node)
if (
isinstance(subnode, FunctionNodes)
and nodes.get_context(subnode) is node
)
]
for subnode in ast.walk(node):
if isinstance(subnode, FunctionNodes):
if nodes.get_context(subnode) is node:
method_nodes.append(subnode.name)
ideal = sorted(method_nodes, key=self._ideal_order, reverse=True)
for existing_order, ideal_order in zip(method_nodes, ideal):
for existing_order, ideal_order in zip(
method_nodes,
ideal,
strict=False,
):
if existing_order != ideal_order:

@@ -434,0 +458,0 @@ self.add_violation(consistency.WrongMethodOrderViolation(node))

import ast
from typing import ClassVar, List, Optional, Sequence
from typing import ClassVar, final
from typing_extensions import final
from wemake_python_styleguide.compat.aliases import AssignNodes, TextNodes
from wemake_python_styleguide.compat.functions import get_assign_targets
from wemake_python_styleguide.logic import nodes, source, walk
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 (
compares,
functions,
ifs,
operators,
pattern_matching,
)

@@ -23,3 +18,2 @@ from wemake_python_styleguide.logic.walrus import get_assigned_expr

from wemake_python_styleguide.violations.consistency import (
CompareOrderViolation,
ConstantCompareViolation,

@@ -33,9 +27,3 @@ ConstantConditionViolation,

FalsyConstantCompareViolation,
InCompareWithSingleItemContainerViolation,
NestedTernaryViolation,
NotOperatorWithCompareViolation,
SimplifiableIfViolation,
UselessLenCompareViolation,
WrongInCompareTypeViolation,
WrongIsCompareViolation,
)

@@ -50,2 +38,4 @@ from wemake_python_styleguide.visitors.base import BaseNodeVisitor

_less_ops: ClassVar[AnyNodes] = (ast.Gt, ast.GtE)
def visit_Compare(self, node: ast.Compare) -> None:

@@ -55,3 +45,2 @@ """Ensures that compares are written correctly."""

self._check_useless_compare(node)
self._check_unpythonic_compare(node)
self._check_heterogeneous_operators(node)

@@ -61,12 +50,2 @@ self._check_reversed_complex_compare(node)

def _is_correct_len(self, sign: ast.cmpop, comparator: ast.AST) -> bool:
"""Helper function which tells what calls to ``len()`` are valid."""
if isinstance(operators.unwrap_unary_node(comparator), ast.Num):
numeric_value = ast.literal_eval(comparator)
if numeric_value == 0:
return False
if numeric_value == 1:
return not isinstance(sign, (ast.GtE, ast.Lt))
return True
def _check_literal_compare(self, node: ast.Compare) -> None:

@@ -89,16 +68,2 @@ last_was_literal = nodes.is_literal(get_assigned_expr(node.left))

def _check_unpythonic_compare(self, node: ast.Compare) -> None:
all_nodes = list(
map(get_assigned_expr, (node.left, *node.comparators)),
)
for index, compare in enumerate(all_nodes):
if not isinstance(compare, ast.Call):
continue
if functions.given_function_called(compare, {'len'}):
ps = index - len(all_nodes) + 1
if not self._is_correct_len(node.ops[ps], node.comparators[ps]):
self.add_violation(UselessLenCompareViolation(node))
def _check_heterogeneous_operators(self, node: ast.Compare) -> None:

@@ -119,6 +84,3 @@ if len(node.ops) == 1:

is_less = all(
isinstance(op, (ast.Gt, ast.GtE))
for op in node.ops
)
is_less = all(isinstance(op, self._less_ops) for op in node.ops)
if not is_less:

@@ -134,15 +96,7 @@ return

_forbidden_for_is: ClassVar[AnyNodes] = (
ast.List,
ast.ListComp,
ast.Dict,
ast.DictComp,
ast.Tuple,
ast.GeneratorExp,
ast.Set,
ast.SetComp,
# We allow `ast.NameConstant`
ast.Num,
*TextNodes,
_eq_compares: ClassVar[AnyNodes] = (
ast.Eq,
ast.NotEq,
ast.Is,
ast.IsNot,
)

@@ -153,7 +107,5 @@

self._check_constant(node.ops[0], node.left)
self._check_is_constant_compare(node.ops[0], node.left)
for op, comparator in zip(node.ops, node.comparators):
for op, comparator in zip(node.ops, node.comparators, strict=False):
self._check_constant(op, comparator)
self._check_is_constant_compare(op, comparator)

@@ -163,11 +115,18 @@ self.generic_visit(node)

def _check_constant(self, op: ast.cmpop, comparator: ast.expr) -> None:
if not isinstance(op, (ast.Eq, ast.NotEq, ast.Is, ast.IsNot)):
if not isinstance(op, self._eq_compares):
return
real = get_assigned_expr(comparator)
if not isinstance(real, (ast.List, ast.Dict, ast.Tuple)):
if not isinstance(real, ast.List | ast.Dict | ast.Tuple):
return
if walk.get_closest_parent(op, ast.Assert):
return # We allow any compares in `assert`
length = len(real.keys) if isinstance(
real, ast.Dict,
) else len(real.elts)
length = (
len(real.keys)
if isinstance(
real,
ast.Dict,
)
else len(real.elts)
)

@@ -177,98 +136,11 @@ if not length:

def _check_is_constant_compare(
self,
op: ast.cmpop,
comparator: ast.expr,
) -> None:
if not isinstance(op, (ast.Is, ast.IsNot)):
return
unwrapped = operators.unwrap_unary_node(
get_assigned_expr(comparator),
)
if isinstance(unwrapped, self._forbidden_for_is):
self.add_violation(WrongIsCompareViolation(comparator))
@final
class WrongComparisonOrderVisitor(BaseNodeVisitor):
"""Restricts comparison where argument doesn't come first."""
_allowed_left_nodes: ClassVar[AnyNodes] = (
ast.Name,
ast.Call,
ast.Attribute,
ast.Subscript,
ast.Await,
)
_special_cases: ClassVar[AnyNodes] = (
ast.In,
ast.NotIn,
)
def visit_Compare(self, node: ast.Compare) -> None:
"""Forbids comparison where argument doesn't come first."""
self._check_ordering(node)
self.generic_visit(node)
def _is_special_case(self, node: ast.Compare) -> bool:
"""
Operators ``in`` and ``not in`` are special cases.
Why? Because it is perfectly fine to use something like:
.. code:: python
if 'key' in some_dict: ...
This should not be an issue.
When there are multiple special operators it is still a separate issue.
"""
return isinstance(node.ops[0], self._special_cases)
def _is_left_node_valid(self, left: ast.AST) -> bool:
if isinstance(left, self._allowed_left_nodes):
return True
if isinstance(left, ast.BinOp):
left_node = self._is_left_node_valid(left.left)
right_node = self._is_left_node_valid(left.right)
return left_node or right_node
return False
def _has_wrong_nodes_on_the_right(
self,
comparators: Sequence[ast.AST],
) -> bool:
for right in map(get_assigned_expr, comparators):
if isinstance(right, self._allowed_left_nodes):
return True
if isinstance(right, ast.BinOp):
return self._has_wrong_nodes_on_the_right([
right.left, right.right,
])
return False
def _check_ordering(self, node: ast.Compare) -> None:
if self._is_left_node_valid(get_assigned_expr(node.left)):
return
if self._is_special_case(node):
return
if len(node.comparators) > 1:
return
if not self._has_wrong_nodes_on_the_right(node.comparators):
return
self.add_violation(CompareOrderViolation(node))
@final
@alias('visit_any_if', (
'visit_If',
'visit_IfExp',
))
@alias(
'visit_any_if',
(
'visit_If',
'visit_IfExp',
),
)
class WrongConditionalVisitor(BaseNodeVisitor):

@@ -279,6 +151,3 @@ """Finds wrong conditional arguments."""

# Constants:
*TextNodes,
ast.Num,
ast.NameConstant,
ast.Constant,
# Collections:

@@ -302,7 +171,2 @@ ast.List,

"""Ensures that ``if`` nodes are using valid conditionals."""
if isinstance(node, ast.If):
self._check_simplifiable_if(node)
else:
self._check_simplifiable_ifexpr(node)
self._check_nested_ifexpr(node)

@@ -318,3 +182,13 @@ self._check_constant_condition(node.test)

def _check_constant_condition(self, node: ast.AST) -> None:
def visit_Match(self, node: ast.Match) -> None:
"""Ensures that ``match`` nodes are using valid conditionals."""
self._check_constant_condition(node.subject, is_match=True)
self.generic_visit(node)
def _check_constant_condition(
self,
node: ast.AST,
*,
is_match: bool = False,
) -> None:
if isinstance(node, ast.BoolOp):

@@ -325,29 +199,19 @@ for condition in node.values:

real_node = operators.unwrap_unary_node(get_assigned_expr(node))
if isinstance(real_node, self._forbidden_nodes):
self.add_violation(ConstantConditionViolation(node))
if is_match and not pattern_matching.is_constant_subject(real_node):
return
if not is_match and not isinstance(
real_node,
self._forbidden_nodes,
):
return
self.add_violation(ConstantConditionViolation(node))
def _check_simplifiable_if(self, node: ast.If) -> None:
if not ifs.is_elif(node) and not ifs.root_if(node):
body_var = self._is_simplifiable_assign(node.body)
else_var = self._is_simplifiable_assign(node.orelse)
if body_var and body_var == else_var:
self.add_violation(SimplifiableIfViolation(node))
def _check_simplifiable_ifexpr(self, node: ast.IfExp) -> None:
conditions = set()
if isinstance(node.body, ast.NameConstant):
conditions.add(node.body.value)
if isinstance(node.orelse, ast.NameConstant):
conditions.add(node.orelse.value)
if conditions == {True, False}:
self.add_violation(SimplifiableIfViolation(node))
def _check_nested_ifexpr(self, node: AnyIf) -> None:
is_nested_in_if = bool(
isinstance(node, ast.If) and
list(walk.get_subnodes_by_type(node.test, ast.IfExp)),
isinstance(node, ast.If)
and list(walk.get_subnodes_by_type(node.test, ast.IfExp)),
)
is_nested_poorly = walk.get_closest_parent(
node, self._forbidden_expression_parents,
node,
self._forbidden_expression_parents,
)

@@ -358,39 +222,4 @@

def _is_simplifiable_assign(
self,
node_body: List[ast.stmt],
) -> Optional[str]:
wrong_length = len(node_body) != 1
if wrong_length or not isinstance(node_body[0], AssignNodes):
return None
if not isinstance(node_body[0].value, ast.NameConstant):
return None
if node_body[0].value.value is None:
return None
targets = get_assign_targets(node_body[0])
if len(targets) != 1:
return None
return source.node_to_string(targets[0])
@final
class UnaryCompareVisitor(BaseNodeVisitor):
"""Checks that unary compare operators are used correctly."""
def visit_UnaryOp(self, node: ast.UnaryOp) -> None:
"""Finds bad `not` usages."""
self._check_incorrect_not(node)
self.generic_visit(node)
def _check_incorrect_not(self, node: ast.UnaryOp) -> None:
if not isinstance(node.op, ast.Not):
return
if isinstance(node.operand, ast.Compare):
self.add_violation(NotOperatorWithCompareViolation(node))
@final
class InCompareSanityVisitor(BaseNodeVisitor):

@@ -404,15 +233,5 @@ """Restricts the incorrect ``in`` compares."""

_wrong_in_comparators: ClassVar[AnyNodes] = (
ast.List,
ast.ListComp,
ast.Dict,
ast.DictComp,
ast.Tuple,
ast.GeneratorExp,
)
def visit_Compare(self, node: ast.Compare) -> None:
"""Ensures that compares are written correctly."""
self._check_multiply_compares(node)
self._check_comparators(node)
self.generic_visit(node)

@@ -425,27 +244,3 @@

def _check_comparators(self, node: ast.Compare) -> None:
for op, comp in zip(node.ops, node.comparators):
if not isinstance(op, self._in_nodes):
continue
real = get_assigned_expr(comp)
self._check_single_item_container(real)
self._check_wrong_comparators(real)
def _check_single_item_container(self, node: ast.AST) -> None:
is_text_violated = isinstance(node, TextNodes) and len(node.s) == 1
is_dict_violated = isinstance(node, ast.Dict) and len(node.keys) == 1
is_iter_violated = (
isinstance(node, (ast.List, ast.Tuple, ast.Set)) and
len(node.elts) == 1
)
if is_text_violated or is_dict_violated or is_iter_violated:
self.add_violation(InCompareWithSingleItemContainerViolation(node))
def _check_wrong_comparators(self, node: ast.AST) -> None:
if isinstance(node, self._wrong_in_comparators):
self.add_violation(WrongInCompareTypeViolation(node))
@final

@@ -460,9 +255,2 @@ class WrongFloatComplexCompareVisitor(BaseNodeVisitor):

def _is_float_or_complex(self, node: ast.AST) -> bool:
node = operators.unwrap_unary_node(node)
return (
isinstance(node, ast.Num) and
isinstance(node.n, (float, complex))
)
def _check_float_complex_compare(self, node: ast.Compare) -> None:

@@ -475,1 +263,8 @@ any_float_or_complex = any(

self.add_violation(FloatComplexCompareViolation(node))
def _is_float_or_complex(self, node: ast.AST) -> bool:
node = operators.unwrap_unary_node(node)
return isinstance(node, ast.Constant) and isinstance(
node.value,
float | complex,
)
import ast
from itertools import takewhile
from typing import ClassVar, Set, cast
from typing import ClassVar, cast, final
from typing_extensions import final
from wemake_python_styleguide.logic.tree import attributes

@@ -27,3 +25,3 @@ from wemake_python_styleguide.types import AnyAccess, AnyNodes

super().__init__(*args, **kwargs)
self._visited_accesses: Set[AnyAccess] = set()
self._visited_accesses: set[AnyAccess] = set()

@@ -47,6 +45,11 @@ def visit_Subscript(self, node: ast.Subscript) -> None:

consecutive_access = cast(Set[AnyAccess], set(takewhile(
self._is_any_access,
attributes.parts(node),
)))
consecutive_access = cast(
set[AnyAccess],
set(
takewhile(
self._is_any_access,
attributes.parts(node),
),
),
)

@@ -53,0 +56,0 @@ self._visited_accesses.update(consecutive_access)

import ast
from typing import List
from typing import final
from typing_extensions import final
from wemake_python_styleguide.logic.complexity.annotations import (

@@ -19,6 +17,9 @@ get_annotation_complexity,

@final
@alias('visit_any_function', (
'visit_FunctionDef',
'visit_AsyncFunctionDef',
))
@alias(
'visit_any_function',
(
'visit_FunctionDef',
'visit_AsyncFunctionDef',
),
)
class AnnotationComplexityVisitor(BaseNodeVisitor):

@@ -38,3 +39,4 @@ """Ensures that annotations are used correctly."""

def _check_function_annotations_complexity(
self, node: AnyFunctionDef,
self,
node: AnyFunctionDef,
) -> None:

@@ -53,3 +55,3 @@ annotations = [

node: ast.AST,
annotations: List[ast.expr],
annotations: list[ast.expr],
) -> None:

@@ -56,0 +58,0 @@ max_complexity = self.options.max_annotation_complexity

import ast
from itertools import takewhile
from typing import Set
from typing import final
from typing_extensions import final
from wemake_python_styleguide.logic.tree.calls import parts

@@ -21,3 +19,3 @@ from wemake_python_styleguide.violations.complexity import (

super().__init__(*args, **kwargs)
self._visited_calls: Set[ast.Call] = set()
self._visited_calls: set[ast.Call] = set()

@@ -36,5 +34,8 @@ def visit_Call(self, node: ast.Call) -> None:

consecutive_calls = set(takewhile(
self._is_call, parts(node),
))
consecutive_calls = set(
takewhile(
self._is_call,
parts(node),
),
)

@@ -41,0 +42,0 @@ self._visited_calls.update(consecutive_calls)

import ast
from collections import defaultdict
from typing import DefaultDict
from typing import final
from typing_extensions import final
from wemake_python_styleguide.logic.naming import access

@@ -57,2 +55,5 @@ from wemake_python_styleguide.logic.nodes import get_parent

def _check_public_attributes(self, node: ast.ClassDef) -> None:
if classes.is_dataclass(node):
return # dataclasses can have any amount of attributes
_, instance_attributes = classes.get_attributes(

@@ -62,7 +63,9 @@ node,

)
attrs_count = len({
attr.attr
for attr in instance_attributes
if access.is_public(attr.attr)
})
attrs_count = len(
{
attr.attr
for attr in instance_attributes
if access.is_public(attr.attr)
},
)

@@ -80,6 +83,9 @@ if attrs_count > self.options.max_attributes:

@final
@alias('visit_any_function', (
'visit_FunctionDef',
'visit_AsyncFunctionDef',
))
@alias(
'visit_any_function',
(
'visit_FunctionDef',
'visit_AsyncFunctionDef',
),
)
class MethodMembersVisitor(BaseNodeVisitor):

@@ -91,3 +97,3 @@ """Counts methods in a single class."""

super().__init__(*args, **kwargs)
self._methods: DefaultDict[ast.ClassDef, int] = defaultdict(int)
self._methods: defaultdict[ast.ClassDef, int] = defaultdict(int)

@@ -94,0 +100,0 @@ def visit_any_function(self, node: AnyFunctionDef) -> None:

import ast
from collections import defaultdict
from typing import DefaultDict, List, Union
from typing import TypeAlias, final
from typing_extensions import TypeAlias, final
from wemake_python_styleguide import constants
from wemake_python_styleguide.compat.aliases import FunctionNodes
from wemake_python_styleguide.compat.nodes import TypeAlias as ast_TypeAlias
from wemake_python_styleguide.compat.types import AnyTry
from wemake_python_styleguide.logic.nodes import get_parent
from wemake_python_styleguide.logic.tree import decorators, functions
from wemake_python_styleguide.logic.nodes import get_context
from wemake_python_styleguide.logic.tree import decorators
from wemake_python_styleguide.types import AnyFunctionDef

@@ -18,12 +17,16 @@ from wemake_python_styleguide.violations import complexity

# Type aliases:
_ModuleMembers: TypeAlias = Union[AnyFunctionDef, ast.ClassDef]
_ReturnLikeStatement: TypeAlias = Union[ast.Return, ast.Yield]
_ModuleMembers: TypeAlias = AnyFunctionDef | ast.ClassDef
_WithTypeParams: TypeAlias = _ModuleMembers | ast_TypeAlias
_ReturnLikeStatement: TypeAlias = ast.Return | ast.Yield
@final
@alias('visit_module_members', (
'visit_ClassDef',
'visit_AsyncFunctionDef',
'visit_FunctionDef',
))
@alias(
'visit_module_members',
(
'visit_ClassDef',
'visit_AsyncFunctionDef',
'visit_FunctionDef',
),
)
class ModuleMembersVisitor(BaseNodeVisitor):

@@ -45,11 +48,12 @@ """Counts classes and functions in a module."""

"""This method increases the number of module members."""
if functions.is_method(getattr(node, 'function_type', '')):
if not isinstance(get_context(node), ast.Module):
return
if isinstance(node, FunctionNodes):
if decorators.has_overload_decorator(node):
return # We don't count `@overload` defs as real defs
if isinstance(
node,
FunctionNodes,
) and decorators.has_overload_decorator(node):
return # We don't count `@overload` defs as real defs
if isinstance(get_parent(node), ast.Module):
self._public_items_count += 1
self._public_items_count += 1

@@ -137,3 +141,3 @@ def _check_decorators_count(self, node: _ModuleMembers) -> None:

super().__init__(*args, **kwargs)
self._if_children: DefaultDict[ast.If, List[ast.If]] = defaultdict(
self._if_children: defaultdict[ast.If, list[ast.If]] = defaultdict(
list,

@@ -159,5 +163,3 @@ )

def _check_elifs(self, node: ast.If) -> None:
has_elif = all(
isinstance(if_node, ast.If) for if_node in node.orelse
)
has_elif = all(isinstance(if_node, ast.If) for if_node in node.orelse)

@@ -182,6 +184,9 @@ if has_elif:

@final
@alias('visit_any_try', (
'visit_Try',
'visit_TryStar',
))
@alias(
'visit_any_try',
(
'visit_Try',
'visit_TryStar',
),
)
class TryExceptVisitor(BaseNodeVisitor):

@@ -194,2 +199,3 @@ """Visits all try/except nodes to ensure that they are not too complex."""

self._check_try_body_length(node)
self._check_exceptions_count(node)
self.generic_visit(node)

@@ -217,8 +223,26 @@

def _check_exceptions_count(self, node: AnyTry) -> None:
for except_handler in node.handlers:
exc_type = except_handler.type
if (
isinstance(exc_type, ast.Tuple)
and len(exc_type.elts) > self.options.max_except_exceptions
):
self.add_violation(
complexity.TooManyExceptExceptionsViolation(
except_handler,
text=str(len(exc_type.elts)),
baseline=self.options.max_except_exceptions,
)
)
@final
@alias('visit_return_like', (
'visit_Return',
'visit_Yield',
))
@alias(
'visit_return_like',
(
'visit_Return',
'visit_Yield',
),
)
class ReturnLikeStatementTupleVisitor(BaseNodeVisitor):

@@ -233,11 +257,13 @@ """Finds too long ``tuples`` in ``yield`` and ``return`` expressions."""

def _check_return_like_values(self, node: _ReturnLikeStatement) -> None:
if isinstance(node.value, ast.Tuple):
if len(node.value.elts) > constants.MAX_LEN_TUPLE_OUTPUT:
self.add_violation(
complexity.TooLongOutputTupleViolation(
node,
text=str(len(node.value.elts)),
baseline=constants.MAX_LEN_TUPLE_OUTPUT,
),
)
if (
isinstance(node.value, ast.Tuple)
and len(node.value.elts) > constants.MAX_LEN_TUPLE_OUTPUT
):
self.add_violation(
complexity.TooLongOutputTupleViolation(
node,
text=str(len(node.value.elts)),
baseline=constants.MAX_LEN_TUPLE_OUTPUT,
),
)

@@ -268,1 +294,34 @@

)
@final
@alias(
'visit_typed_params',
(
'visit_ClassDef',
'visit_AsyncFunctionDef',
'visit_FunctionDef',
'visit_TypeAlias',
),
)
class TypeParamsVisitor(BaseNodeVisitor): # pragma: >=3.12 cover
"""Finds wrong type parameters."""
def visit_typed_params(self, node: _WithTypeParams) -> None:
"""Finds all objects with ``type_params``."""
self._check_type_params_count(node)
self.generic_visit(node)
def _check_type_params_count(
self,
node: _WithTypeParams,
) -> None:
type_params = getattr(node, 'type_params', [])
if len(type_params) > self.options.max_type_params:
self.add_violation(
complexity.TooManyTypeParamsViolation(
node,
text=str(len(type_params)),
baseline=self.options.max_type_params,
)
)
import ast
from collections import defaultdict
from typing import ClassVar, DefaultDict, List, Mapping, Tuple, Type, Union
from collections.abc import Mapping
from typing import ClassVar, TypeAlias, final
from typing_extensions import TypeAlias, final
from wemake_python_styleguide.logic.arguments import special_args
from wemake_python_styleguide.logic.complexity import cognitive

@@ -14,3 +14,3 @@ from wemake_python_styleguide.logic.complexity.functions import (

from wemake_python_styleguide.logic.naming import access
from wemake_python_styleguide.logic.nodes import get_parent
from wemake_python_styleguide.logic.nodes import get_context, get_parent
from wemake_python_styleguide.logic.tree import functions

@@ -29,9 +29,6 @@ from wemake_python_styleguide.types import (

_AnyFunctionCounter: TypeAlias = Union[
FunctionCounter,
FunctionCounterWithLambda,
]
_CheckRule: TypeAlias = Tuple[_AnyFunctionCounter, int, Type[BaseViolation]]
_AnyFunctionCounter: TypeAlias = FunctionCounter | FunctionCounterWithLambda
_CheckRule: TypeAlias = tuple[_AnyFunctionCounter, int, type[BaseViolation]]
_NodeTypeHandler: TypeAlias = Mapping[
Union[type, Tuple[type, ...]],
type | tuple[type, ...],
FunctionCounter,

@@ -45,5 +42,3 @@ ]

_not_contain_locals: ClassVar[AnyNodes] = (
ast.comprehension,
)
_not_contain_locals: ClassVar[AnyNodes] = (ast.comprehension,)

@@ -55,4 +50,10 @@ def __init__(self) -> None:

"""Checks the number of the arguments in a function."""
self.metrics.arguments[node] = len(functions.get_all_arguments(node))
if functions.is_overload(node):
return # we allow any number of params in overload defs
all_args = functions.get_all_arguments(node)
self.metrics.arguments[node] = len(
special_args.clean_special_argument(node, all_args),
)
def check_function_complexity(self, node: AnyFunctionDef) -> None:

@@ -84,2 +85,5 @@ """

if get_context(variable_def) is not function:
return
parent = get_parent(variable_def)

@@ -97,5 +101,7 @@ no_locals = self._not_contain_locals

) -> None:
if isinstance(sub_node, ast.Name):
if isinstance(sub_node.ctx, ast.Store):
self._update_variables(node, sub_node)
if isinstance(sub_node, ast.Name) and isinstance(
sub_node.ctx,
ast.Store,
):
self._update_variables(node, sub_node)

@@ -116,6 +122,9 @@ error_counters: _NodeTypeHandler = {

@final
@alias('visit_any_function', (
'visit_AsyncFunctionDef',
'visit_FunctionDef',
))
@alias(
'visit_any_function',
(
'visit_AsyncFunctionDef',
'visit_FunctionDef',
),
)
class FunctionComplexityVisitor(BaseNodeVisitor):

@@ -179,3 +188,3 @@ """

def _function_checks(self) -> List[_CheckRule]:
def _function_checks(self) -> list[_CheckRule]:
return [

@@ -215,6 +224,9 @@ (

@final
@alias('visit_any_function', (
'visit_AsyncFunctionDef',
'visit_FunctionDef',
))
@alias(
'visit_any_function',
(
'visit_AsyncFunctionDef',
'visit_FunctionDef',
),
)
class CognitiveComplexityVisitor(BaseNodeVisitor):

@@ -226,3 +238,3 @@ """Used to count cognitive score and average module complexity."""

super().__init__(*args, **kwargs)
self._functions: DefaultDict[AnyFunctionDef, int] = defaultdict(int)
self._functions: defaultdict[AnyFunctionDef, int] = defaultdict(int)

@@ -229,0 +241,0 @@ def visit_any_function(self, node: AnyFunctionDef) -> None:

import ast
from typing import final
from typing_extensions import final
from wemake_python_styleguide.types import AnyImport, ConfigurationOptions
from wemake_python_styleguide.options.validation import ValidatedOptions
from wemake_python_styleguide.types import AnyImport
from wemake_python_styleguide.violations import complexity

@@ -18,3 +18,3 @@ from wemake_python_styleguide.violations.base import ErrorCallback

error_callback: ErrorCallback,
options: ConfigurationOptions,
options: ValidatedOptions,
) -> None:

@@ -21,0 +21,0 @@ self._error_callback = error_callback

@@ -13,7 +13,6 @@ """

from statistics import median
from typing import DefaultDict, List, Set
from typing import final
from typing_extensions import final
from wemake_python_styleguide.compat.aliases import FunctionNodes
from wemake_python_styleguide.compat.nodes import TypeAlias as ast_TypeAlias
from wemake_python_styleguide.violations.complexity import (

@@ -49,4 +48,4 @@ JonesScoreViolation,

super().__init__(*args, **kwargs)
self._lines: DefaultDict[int, List[ast.AST]] = defaultdict(list)
self._to_ignore: Set[ast.AST] = set()
self._lines: defaultdict[int, list[ast.AST]] = defaultdict(list)
self._to_ignore: set[ast.AST] = set()

@@ -62,5 +61,8 @@ def visit(self, node: ast.AST) -> None:

if line_number is not None and not is_ignored:
if not self._maybe_ignore_child(node):
self._lines[line_number].append(node)
if (
line_number is not None
and not is_ignored
and not self._maybe_ignore_child(node)
):
self._lines[line_number].append(node)

@@ -101,2 +103,4 @@ self.generic_visit(node)

self._to_ignore.update(ast.walk(node.annotation))
if isinstance(node, ast_TypeAlias): # pragma: >=3.12 cover
self._to_ignore.update(ast.walk(node.value))
return node in self._to_ignore
import ast
from typing import final
from typing_extensions import final
from wemake_python_styleguide.compat.aliases import FunctionNodes

@@ -19,6 +18,9 @@ from wemake_python_styleguide.constants import NESTED_FUNCTIONS_WHITELIST

@final
@alias('visit_any_function', (
'visit_FunctionDef',
'visit_AsyncFunctionDef',
))
@alias(
'visit_any_function',
(
'visit_FunctionDef',
'visit_AsyncFunctionDef',
),
)
class NestedComplexityVisitor(BaseNodeVisitor):

@@ -25,0 +27,0 @@ """

import ast
from typing import ClassVar
from typing import ClassVar, final
from typing_extensions import final
from wemake_python_styleguide.compat.aliases import FunctionNodes

@@ -16,24 +14,27 @@ from wemake_python_styleguide.logic.nodes import get_parent

@final
@alias('visit_line_expression', (
'visit_Try',
'visit_TryStar',
'visit_Match',
'visit_ExceptHandler',
'visit_For',
'visit_With',
'visit_While',
'visit_If',
'visit_Raise',
'visit_Return',
'visit_Continue',
'visit_Break',
'visit_Assign',
'visit_Expr',
'visit_Pass',
'visit_ClassDef',
'visit_FunctionDef',
'visit_AsyncFor',
'visit_AsyncWith',
'visit_AsyncFunctionDef',
))
@alias(
'visit_line_expression',
(
'visit_Try',
'visit_TryStar',
'visit_Match',
'visit_ExceptHandler',
'visit_For',
'visit_With',
'visit_While',
'visit_If',
'visit_Raise',
'visit_Return',
'visit_Continue',
'visit_Break',
'visit_Assign',
'visit_Expr',
'visit_Pass',
'visit_ClassDef',
'visit_FunctionDef',
'visit_AsyncFor',
'visit_AsyncWith',
'visit_AsyncFunctionDef',
),
)
class OffsetVisitor(BaseNodeVisitor):

@@ -62,6 +63,6 @@ """Checks offset values for several nodes."""

is_function_ellipsis = (
isinstance(get_parent(node), (*FunctionNodes, ast.ClassDef)) and
isinstance(node, ast.Expr) and
isinstance(node.value, ast.Constant) and
node.value.value is Ellipsis
isinstance(get_parent(node), (*FunctionNodes, ast.ClassDef))
and isinstance(node, ast.Expr)
and isinstance(node.value, ast.Constant)
and node.value.value is Ellipsis
)

@@ -68,0 +69,0 @@ if is_function_ellipsis:

import ast
from collections import defaultdict
from typing import (
Callable,
ClassVar,
DefaultDict,
FrozenSet,
List,
Tuple,
Union,
)
from collections.abc import Callable
from typing import ClassVar, TypeAlias, final
from typing_extensions import TypeAlias, final
from wemake_python_styleguide.compat.aliases import FunctionNodes

@@ -19,3 +10,3 @@ from wemake_python_styleguide.logic import source, walk

from wemake_python_styleguide.logic.tree import annotations
from wemake_python_styleguide.types import AnyNodes, AnyText, AnyTextPrimitive
from wemake_python_styleguide.types import AnyNodes, AnyTextPrimitive
from wemake_python_styleguide.violations import complexity

@@ -25,12 +16,15 @@ from wemake_python_styleguide.visitors import base, decorators

#: We use these types to store the number of nodes usage in different contexts.
_Expressions: TypeAlias = DefaultDict[str, List[ast.AST]]
_FunctionExpressions: TypeAlias = DefaultDict[ast.AST, _Expressions]
_StringConstants: TypeAlias = FrozenSet[Union[str, bytes]]
_Expressions: TypeAlias = defaultdict[str, list[ast.AST]]
_FunctionExpressions: TypeAlias = defaultdict[ast.AST, _Expressions]
_StringConstants: TypeAlias = frozenset[str | bytes]
@final
@decorators.alias('visit_any_string', (
'visit_Str',
'visit_Bytes',
))
@decorators.alias(
'visit_any_string',
(
'visit_Str',
'visit_Bytes',
),
)
class StringOveruseVisitor(base.BaseNodeVisitor):

@@ -45,23 +39,26 @@ """

_ignored_string_constants: ClassVar[_StringConstants] = frozenset((
' ',
'.',
',',
'',
'\n',
'\r\n',
'\t',
'|',
'"',
"'",
b'"',
b"'",
b' ',
b'.',
b',',
b'',
b'\n',
b'\r\n',
b'\t',
))
_ignored_string_constants: ClassVar[_StringConstants] = frozenset(
(
' ',
'.',
',',
'',
'\n',
'\r\n',
'\t',
'|',
'"',
"'",
'...',
b'"',
b"'",
b' ',
b'.',
b',',
b'',
b'\n',
b'\r\n',
b'\t',
),
)

@@ -71,7 +68,8 @@ def __init__(self, *args, **kwargs) -> None:

super().__init__(*args, **kwargs)
self._string_constants: DefaultDict[
AnyTextPrimitive, int,
self._string_constants: defaultdict[
AnyTextPrimitive,
int,
] = defaultdict(int)
def visit_any_string(self, node: AnyText) -> None:
def visit_any_string(self, node: ast.Constant) -> None:
"""Restricts to over-use string constants."""

@@ -81,3 +79,3 @@ self._check_string_constant(node)

def _check_string_constant(self, node: AnyText) -> None:
def _check_string_constant(self, node: ast.Constant) -> None:
if annotations.is_annotation(node):

@@ -88,6 +86,6 @@ return

# they are overused.
if node.s in self._ignored_string_constants:
if node.value in self._ignored_string_constants:
return
self._string_constants[node.s] += 1
self._string_constants[node.value] += 1

@@ -120,3 +118,2 @@ def _post_visit(self) -> None:

ast.Lambda,
ast.DictComp,

@@ -132,3 +129,3 @@ ast.Dict,

_ignore_predicates: Tuple[Callable[[ast.AST], bool], ...] = (
_ignore_predicates: tuple[Callable[[ast.AST], bool], ...] = (
overuses.is_decorator,

@@ -135,0 +132,0 @@ overuses.is_self,

import ast
from collections import defaultdict
from functools import reduce
from typing import ClassVar, DefaultDict, List, Mapping, Set, Type
from collections import Counter
from collections.abc import Mapping
from typing import ClassVar, TypeAlias, final
from typing_extensions import Final, TypeAlias, final
from wemake_python_styleguide.compat.aliases import ForNodes
from wemake_python_styleguide.compat.nodes import TryStar
from wemake_python_styleguide.logic import source, walk
from wemake_python_styleguide.logic.nodes import get_parent
from wemake_python_styleguide.logic.tree import ifs, keywords, operators
from wemake_python_styleguide.logic.tree.compares import CompareBounds
from wemake_python_styleguide.logic.tree.functions import given_function_called
from wemake_python_styleguide.types import AnyIf, AnyLoop, AnyNodes
from wemake_python_styleguide.logic import source
from wemake_python_styleguide.logic.tree import (
attributes,
compares,
ifs,
operators,
)
from wemake_python_styleguide.types import AnyIf, AnyNodes
from wemake_python_styleguide.violations import (
best_practices,
consistency,
refactoring,

@@ -24,211 +21,89 @@ )

_OperatorPairs: TypeAlias = Mapping[Type[ast.boolop], Type[ast.cmpop]]
_ELSE_NODES: Final = (*ForNodes, ast.While, ast.Try, TryStar)
_OperatorPairs: TypeAlias = Mapping[type[ast.boolop], type[ast.cmpop]]
# TODO: move to logic
def _duplicated_isinstance_call(node: ast.BoolOp) -> List[str]:
counter: DefaultDict[str, int] = defaultdict(int)
@final
@alias(
'visit_any_if',
(
'visit_If',
'visit_IfExp',
),
)
class IfStatementVisitor(BaseNodeVisitor):
"""Checks single and consecutive ``if`` statement nodes."""
for call in node.values:
if not isinstance(call, ast.Call) or len(call.args) != 2:
continue
if not given_function_called(call, {'isinstance'}):
continue
isinstance_object = source.node_to_string(call.args[0])
counter[isinstance_object] += 1
return [
node_name
for node_name, count in counter.items()
if count > 1
]
# TODO: move to logic
def _get_duplicate_names(variables: List[Set[str]]) -> Set[str]:
return reduce(
lambda acc, element: acc.intersection(element),
variables,
_nodes_to_check: ClassVar[AnyNodes] = (
ast.Name,
ast.Attribute,
ast.Subscript,
ast.Constant,
ast.List,
ast.Dict,
ast.Tuple,
ast.Set,
)
def __init__(self, *args, **kwargs) -> None:
"""Save visited ``if`` nodes."""
super().__init__(*args, **kwargs)
self._finder = ifs.NegatedIfConditions()
@final
@alias('visit_any_if', (
'visit_If',
'visit_IfExp',
))
class IfStatementVisitor(BaseNodeVisitor):
"""Checks single and consecutive ``if`` statement nodes."""
def visit_any_if(self, node: AnyIf) -> None:
"""Checks ``if`` nodes and expressions."""
self._check_negated_conditions(node)
self._check_useless_len(node)
if isinstance(node, ast.If):
self._check_multiline_conditions(node)
self._check_simplifiable_returning_if(node)
self._check_repeated_conditions(node)
self._check_useless_ternary(node)
self.generic_visit(node)
def _check_negated_conditions(self, node: AnyIf) -> None:
if isinstance(node, ast.If) and not ifs.has_else(node):
return
if isinstance(node.test, ast.UnaryOp):
if isinstance(node.test.op, ast.Not):
self.add_violation(refactoring.NegatedConditionsViolation(node))
elif isinstance(node.test, ast.Compare):
if any(isinstance(elem, ast.NotEq) for elem in node.test.ops):
self.add_violation(refactoring.NegatedConditionsViolation(node))
def _check_useless_len(self, node: AnyIf) -> None:
if isinstance(node.test, ast.Call):
if given_function_called(node.test, {'len'}):
self.add_violation(refactoring.UselessLenCompareViolation(node))
def _check_multiline_conditions(self, node: ast.If) -> None:
"""Checks multiline conditions ``if`` statement nodes."""
start_lineno = getattr(node, 'lineno', None)
for sub_nodes in ast.walk(node.test):
sub_lineno = getattr(sub_nodes, 'lineno', None)
if sub_lineno is not None and sub_lineno > start_lineno:
self.add_violation(
consistency.MultilineConditionsViolation(node),
)
break
def _check_simplifiable_returning_if(self, node: ast.If) -> None:
body = node.body
simple_if_and_root = not (ifs.has_elif(node) or ifs.is_elif(node))
if keywords.is_simple_return(body) and simple_if_and_root:
if ifs.has_else(node):
else_body = node.orelse
if keywords.is_simple_return(else_body):
self.add_violation(
refactoring.SimplifiableReturningIfViolation(node),
)
return
self._check_simplifiable_returning_parent(node)
def _check_simplifiable_returning_parent(self, node: ast.If) -> None:
parent = get_parent(node)
if isinstance(parent, _ELSE_NODES):
body = parent.body + parent.orelse
else:
body = getattr(parent, 'body', [node])
next_index_in_parent = body.index(node) + 1
if keywords.next_node_returns_bool(body, next_index_in_parent):
for subnode in self._finder.negated_nodes(node):
self.add_violation(
refactoring.SimplifiableReturningIfViolation(node),
refactoring.NegatedConditionsViolation(subnode),
)
def _check_repeated_conditions(self, node: AnyIf) -> None:
if not isinstance(node, ast.If):
return
@final
@alias('visit_any_loop', (
'visit_For',
'visit_AsyncFor',
'visit_While',
))
class UselessElseVisitor(BaseNodeVisitor):
"""Ensures that ``else`` is used correctly for different nodes."""
#: Nodes that break or return the execution flow.
_returning_nodes: ClassVar[AnyNodes] = (
ast.Break,
ast.Raise,
ast.Return,
ast.Continue,
)
def __init__(self, *args, **kwargs) -> None:
"""We need to store visited ``if`` not to duplicate violations."""
super().__init__(*args, **kwargs)
self._visited_ifs: Set[ast.If] = set()
def visit_If(self, node: ast.If) -> None:
"""Checks ``if`` statements."""
self._check_useless_if_else(node)
self.generic_visit(node)
def visit_Try(self, node: ast.Try) -> None:
"""Checks exception handling."""
self._check_useless_try_else(node)
self.generic_visit(node)
def visit_any_loop(self, node: AnyLoop) -> None:
"""Checks any loops."""
self._check_useless_loop_else(node)
self.generic_visit(node)
def _check_useless_if_else(self, node: ast.If) -> None:
real_ifs = []
for chained_if in ifs.chain(node):
if isinstance(chained_if, ast.If):
if chained_if in self._visited_ifs:
return
self._visited_ifs.update({chained_if})
real_ifs.append(chained_if)
continue
previous_has_returns = all(
ifs.has_nodes(self._returning_nodes, real_if.body)
for real_if in real_ifs
)
current_has_returns = ifs.has_nodes(
self._returning_nodes, chained_if,
)
if previous_has_returns and current_has_returns:
conditions = [
source.node_to_string(chained.test)
for chained in ifs.chain(node)
if isinstance(chained, ast.If)
]
for condition, times in Counter(conditions).items():
if times > 1:
self.add_violation(
refactoring.UselessReturningElseViolation(chained_if[0]),
refactoring.DuplicateIfConditionViolation(
node,
text=condition,
)
)
def _check_useless_try_else(self, node: ast.Try) -> None:
if not node.orelse or node.finalbody:
# `finally` cancels this rule.
# Because refactoring `try` with `else` and `finally`
# by moving `else` body after `finally` will change
# the execution order.
def _check_useless_ternary(self, node: AnyIf) -> None:
if not isinstance(node, ast.IfExp):
return
all_except_returning = all(
walk.is_contained(except_, self._returning_nodes)
for except_ in node.handlers
)
else_returning = any(
walk.is_contained(sub, self._returning_nodes)
for sub in node.orelse
)
if all_except_returning and else_returning:
self.add_violation(refactoring.UselessReturningElseViolation(node))
comp = node.test
if not isinstance(comp, ast.Compare) or len(comp.ops) > 1:
return # We only check for compares with exactly one op
def _check_useless_loop_else(self, node: AnyLoop) -> None:
if not node.orelse:
return
# An else statement makes sense if we
# want to execute something after breaking
# out of the loop without writing more code
has_break = any(
walk.is_contained(sub, ast.Break)
for sub in node.body
)
if has_break:
return
body_returning = any(
walk.is_contained(sub, self._returning_nodes[1:])
for sub in node.body
)
else_returning = any(
walk.is_contained(sub, self._returning_nodes)
for sub in node.orelse
)
if body_returning and else_returning:
self.add_violation(refactoring.UselessReturningElseViolation(node))
if not attributes.only_consists_of_parts(
node.body,
self._nodes_to_check,
) or not attributes.only_consists_of_parts(
node.orelse,
self._nodes_to_check,
):
return # Only simple nodes are allowed on left and right parts
if compares.is_useless_ternary(
node,
comp.ops[0],
comp.left,
comp.comparators[0],
):
self.add_violation(refactoring.UselessTernaryViolation(node))
@final

@@ -241,4 +116,3 @@ class BooleanConditionVisitor(BaseNodeVisitor):

super().__init__(*args, **kwargs)
self._same_nodes: List[ast.BoolOp] = []
self._isinstance_calls: List[ast.BoolOp] = []
self._same_nodes: list[ast.BoolOp] = []

@@ -248,3 +122,2 @@ def visit_BoolOp(self, node: ast.BoolOp) -> None:

self._check_same_elements(node)
self._check_isinstance_calls(node)
self.generic_visit(node)

@@ -255,3 +128,3 @@

node: ast.BoolOp,
) -> List[str]:
) -> list[str]:
# We need to make sure that we do not visit

@@ -283,56 +156,29 @@ # one chained `BoolOp` elements twice:

def _check_isinstance_calls(self, node: ast.BoolOp) -> None:
if not isinstance(node.op, ast.Or):
return
for var_name in _duplicated_isinstance_call(node):
self.add_violation(
refactoring.UnmergedIsinstanceCallsViolation(
node,
text=var_name,
),
)
@final
class ImplicitBoolPatternsVisitor(BaseNodeVisitor):
"""Is used to find implicit patterns that are formed by boolops."""
class MatchVisitor(BaseNodeVisitor):
"""Visits conditions in pattern matching."""
_allowed: ClassVar[_OperatorPairs] = {
ast.And: ast.NotEq,
ast.Or: ast.Eq,
}
def visit_BoolOp(self, node: ast.BoolOp) -> None:
"""Checks ``and`` and ``or`` don't form implicit anti-patterns."""
self._check_implicit_in(node)
self._check_implicit_complex_compare(node)
def visit_Match(self, node: ast.Match) -> None:
"""Finds issues in PM conditions."""
self._check_duplicate_cases(node)
self.generic_visit(node)
def _check_implicit_in(self, node: ast.BoolOp) -> None:
variables: List[Set[str]] = []
def _check_duplicate_cases(self, node: ast.Match) -> None:
conditions = [self._parse_case(case_node) for case_node in node.cases]
for condition, times in Counter(conditions).items():
if times > 1:
self.add_violation(
refactoring.DuplicateCasePatternViolation(
node,
text=condition,
)
)
for cmp in node.values:
if not isinstance(cmp, ast.Compare) or len(cmp.ops) != 1:
return
if not isinstance(cmp.ops[0], self._allowed[node.op.__class__]):
return
def _parse_case(self, node: ast.match_case) -> str:
pattern = source.node_to_string(node.pattern)
guard = source.node_to_string(node.guard) if node.guard else ''
return f'{pattern} if {guard}' if guard else pattern
variables.append({source.node_to_string(cmp.left)})
for duplicate in _get_duplicate_names(variables):
self.add_violation(
refactoring.ImplicitInConditionViolation(node, text=duplicate),
)
def _check_implicit_complex_compare(self, node: ast.BoolOp) -> None:
if not isinstance(node.op, ast.And):
return
if not CompareBounds(node).is_valid():
self.add_violation(
consistency.ImplicitComplexCompareViolation(node),
)
@final

@@ -344,6 +190,7 @@ class ChainedIsVisitor(BaseNodeVisitor):

"""Checks for chained 'is' operators in comparisons."""
if len(node.ops) > 1:
if all(isinstance(op, ast.Is) for op in node.ops):
self.add_violation(refactoring.ChainedIsViolation(node))
if len(node.ops) > 1 and all(
isinstance(operator, ast.Is) for operator in node.ops
):
self.add_violation(refactoring.ChainedIsViolation(node))
self.generic_visit(node)
import ast
from typing import Final, final
from typing_extensions import Final, final
from wemake_python_styleguide.logic.tree import attributes

@@ -21,6 +20,9 @@ from wemake_python_styleguide.types import AnyFunctionDef

@final
@alias('visit_any_function', (
'visit_FunctionDef',
'visit_AsyncFunctionDef',
))
@alias(
'visit_any_function',
(
'visit_FunctionDef',
'visit_AsyncFunctionDef',
),
)
class WrongDecoratorVisitor(BaseNodeVisitor):

@@ -27,0 +29,0 @@ """Checks decorators's correctness."""

import ast
from collections import Counter
from typing import ClassVar, Set
from typing import ClassVar, final
from typing_extensions import final
from wemake_python_styleguide.compat.aliases import FunctionNodes

@@ -11,15 +8,7 @@ from wemake_python_styleguide.compat.types import AnyTry

from wemake_python_styleguide.logic.tree import exceptions
from wemake_python_styleguide.logic.walk import is_contained
from wemake_python_styleguide.types import AnyNodes
from wemake_python_styleguide.violations.best_practices import (
BaseExceptionViolation,
DuplicateExceptionViolation,
IncorrectExceptOrderViolation,
LoopControlFinallyViolation,
NonTrivialExceptViolation,
TryExceptMultipleReturnPathViolation,
)
from wemake_python_styleguide.violations.consistency import (
UselessExceptCaseViolation,
)
from wemake_python_styleguide.violations.refactoring import (

@@ -34,22 +23,16 @@ NestedTryViolation,

@final
@alias('visit_any_try', (
'visit_Try',
'visit_TryStar',
))
@alias(
'visit_any_try',
(
'visit_Try',
'visit_TryStar',
),
)
class WrongTryExceptVisitor(BaseNodeVisitor):
"""Responsible for examining ``try`` and friends."""
_bad_returning_nodes: ClassVar[AnyNodes] = (
ast.Return,
ast.Raise,
ast.Break,
)
def visit_any_try(self, node: AnyTry) -> None:
"""Used for find ``finally`` in ``try`` blocks without ``except``."""
self._check_if_needs_except(node)
self._check_duplicate_exceptions(node)
self._check_return_path(node)
self._check_exception_order(node)
self._check_break_or_continue_in_finally(node)
self.generic_visit(node)

@@ -69,26 +52,6 @@

def _check_duplicate_exceptions(self, node: AnyTry) -> None:
exceptions_list = exceptions.get_all_exception_names(node)
for exc_name, count in Counter(exceptions_list).items():
if count > 1:
self.add_violation(
DuplicateExceptionViolation(node, text=exc_name),
)
def _check_return_path(self, node: AnyTry) -> None:
find_returning = exceptions.find_returning_nodes
try_has, except_has, else_has, finally_has = find_returning(
node, self._bad_returning_nodes,
)
if finally_has and (try_has or except_has or else_has):
self.add_violation(TryExceptMultipleReturnPathViolation(node))
elif else_has and try_has:
self.add_violation(TryExceptMultipleReturnPathViolation(node))
def _check_exception_order(self, node: AnyTry) -> None:
built_in_exceptions = exceptions.traverse_exception(BaseException)
exceptions_list = exceptions.get_all_exception_names(node)
seen: Set[str] = set()
seen: set[str] = set()

@@ -104,13 +67,3 @@ for exception in exceptions_list:

def _check_break_or_continue_in_finally(self, node: AnyTry) -> None:
has_wrong_nodes = any(
is_contained(line, (ast.Break, ast.Continue))
for line in node.finalbody
)
if has_wrong_nodes:
# TryStar cannot have loop control in its body, ignoring:
self.add_violation(LoopControlFinallyViolation(node))
@final

@@ -135,4 +88,2 @@ class NestedTryBlocksVisitor(BaseNodeVisitor):

_base_exception: ClassVar[str] = 'BaseException'
_trivial_except_arg_nodes: ClassVar[AnyNodes] = (ast.Name, ast.Attribute)

@@ -142,33 +93,5 @@

"""Checks all ``ExceptionHandler`` nodes."""
self._check_useless_except(node)
self._check_exception_type(node)
self._check_except_expression(node)
self.generic_visit(node)
def _check_useless_except(self, node: ast.ExceptHandler) -> None:
if len(node.body) != 1:
return
body = node.body[0]
if not isinstance(body, ast.Raise):
return
if isinstance(body.exc, ast.Call):
return
if isinstance(body.exc, ast.Name) and node.name:
if body.exc.id != node.name:
return
self.add_violation(UselessExceptCaseViolation(node))
def _check_exception_type(self, node: ast.ExceptHandler) -> None:
exception_name = node.type
if exception_name is None:
return
exception_id = getattr(exception_name, 'id', None)
if exception_id == self._base_exception:
self.add_violation(BaseExceptionViolation(node))
def _check_except_expression(self, node: ast.ExceptHandler) -> None:

@@ -183,6 +106,6 @@ # Catch-all 'except' is actually okay in this case

if isinstance(node.type, ast.Tuple):
all_elements_are_trivial = all((
all_elements_are_trivial = all(
isinstance(element, self._trivial_except_arg_nodes)
for element in node.type.elts
))
)
if all_elements_are_trivial:

@@ -189,0 +112,0 @@ return

import ast
from collections.abc import Mapping
from contextlib import suppress
from typing import ClassVar, Dict, FrozenSet, List, Mapping, Union
from typing import ClassVar, TypeAlias, final
from typing_extensions import TypeAlias, final
from wemake_python_styleguide.compat.aliases import (
ForNodes,
FunctionNodes,
TextNodes,
)
from wemake_python_styleguide.constants import (
FUNCTIONS_BLACKLIST,
LITERALS_BLACKLIST,
)
from wemake_python_styleguide.logic import nodes, source, walk
from wemake_python_styleguide.logic import nodes, walk
from wemake_python_styleguide.logic.arguments import function_args

@@ -32,8 +29,7 @@ from wemake_python_styleguide.logic.naming import access

)
from wemake_python_styleguide.violations import consistency, naming, oop
from wemake_python_styleguide.violations import naming, oop
from wemake_python_styleguide.violations.best_practices import (
BooleanPositionalArgumentViolation,
ComplexDefaultValueViolation,
FloatingNanViolation,
GetterWithoutReturnViolation,
ProblematicFunctionParamsViolation,
StopIterationInsideGeneratorViolation,

@@ -48,3 +44,2 @@ WrongFunctionCallViolation,

UselessLambdaViolation,
WrongIsinstanceWithTupleViolation,
)

@@ -54,8 +49,5 @@ from wemake_python_styleguide.visitors import base, decorators

#: Things we treat as local variables.
_LocalVariable: TypeAlias = Union[ast.Name, ast.ExceptHandler]
_LocalVariable: TypeAlias = ast.Name | ast.ExceptHandler
#: Function definitions with name and arity:
_Defs: TypeAlias = Mapping[str, int]
@final

@@ -69,22 +61,5 @@ class WrongFunctionCallVisitor(base.BaseNodeVisitor):

_functions: ClassVar[_Defs] = {
'getattr': 3,
'setattr': 3,
}
_postfixes: ClassVar[_Defs] = {
# dict methods:
'.get': 2,
'.pop': 2,
'.setdefault': 2,
# list methods:
'.insert': 2,
}
def visit_Call(self, node: ast.Call) -> None:
"""Used to find ``FUNCTIONS_BLACKLIST`` calls."""
self._check_wrong_function_called(node)
self._check_boolean_arguments(node)
self._check_isinstance_call(node)

@@ -99,3 +74,4 @@ if functions.given_function_called(node, {'super'}):

function_name = functions.given_function_called(
node, FUNCTIONS_BLACKLIST,
node,
FUNCTIONS_BLACKLIST,
)

@@ -107,29 +83,2 @@ if function_name:

def _check_boolean_arguments(self, node: ast.Call) -> None:
if len(node.args) == 1 and not node.keywords:
return # Calls with single boolean argument are allowed
for arg in node.args:
if not isinstance(arg, ast.NameConstant):
continue
is_ignored = self._is_call_ignored(node)
# We do not check for `None` values here:
if not is_ignored and arg.value in {True, False}:
self.add_violation(
BooleanPositionalArgumentViolation(
arg, text=str(arg.value),
),
)
def _check_isinstance_call(self, node: ast.Call) -> None:
function_name = functions.given_function_called(node, {'isinstance'})
if not function_name or len(node.args) != 2:
return
if isinstance(node.args[1], ast.Tuple):
if len(node.args[1].elts) == 1:
self.add_violation(WrongIsinstanceWithTupleViolation(node))
def _check_super_context(self, node: ast.Call) -> None:

@@ -162,43 +111,4 @@ parent_context = nodes.get_context(node)

def _is_call_ignored(self, node: ast.Call) -> bool:
call = source.node_to_string(node.func)
func_called = functions.given_function_called(
node, self._functions.keys(),
)
return bool(
func_called and len(node.args) == self._functions[func_called],
) or any(
call.endswith(post)
for post in self._postfixes
if len(node.args) == self._postfixes[post]
)
@final
class FloatingNanCallVisitor(base.BaseNodeVisitor):
"""Ensure that NaN explicitly acquired."""
_nan_variants = frozenset(('nan', b'nan'))
def visit_Call(self, node: ast.Call) -> None:
"""Used to find ``float("NaN")`` calls."""
self._check_floating_nan(node)
self.generic_visit(node)
def _check_floating_nan(self, node: ast.Call) -> None:
if len(node.args) != 1:
return
if not isinstance(node.args[0], (ast.Str, ast.Bytes)):
return
if not functions.given_function_called(node, 'float'):
return
if node.args[0].s.lower() in self._nan_variants:
self.add_violation(FloatingNanViolation(node))
@final
class WrongFunctionCallContextVisitor(base.BaseNodeVisitor):

@@ -225,5 +135,5 @@ """Ensure that we call several functions in the correct context."""

if_exp_inside_with = (
isinstance(parent_node, ast.IfExp) and
isinstance(nodes.get_parent(parent_node), ast.withitem)
if_exp_inside_with = isinstance(parent_node, ast.IfExp) and isinstance(
nodes.get_parent(parent_node),
ast.withitem,
)

@@ -253,9 +163,8 @@

is_one_arg_range = (
args_len == 1 and
isinstance(node.args[0], ast.Call) and
functions.given_function_called(node.args[0], {'len'})
args_len == 1
and isinstance(node.args[0], ast.Call)
and functions.given_function_called(node.args[0], {'len'})
)
is_two_args_range = (
self._is_multiple_args_range_with_len(node) and
args_len == 2
self._is_multiple_args_range_with_len(node) and args_len == 2
)

@@ -266,6 +175,6 @@ # for three args add violation

is_three_args_range = (
self._is_multiple_args_range_with_len(node) and
args_len == 3 and
isinstance(step_arg, ast.Num) and
abs(step_arg.n) == 1
self._is_multiple_args_range_with_len(node)
and args_len == 3
and isinstance(step_arg, ast.Constant)
and abs(step_arg.value) == 1
)

@@ -277,5 +186,5 @@ if any([is_one_arg_range, is_two_args_range, is_three_args_range]):

return bool(
len(node.args) in {2, 3} and
isinstance(node.args[1], ast.Call) and
functions.given_function_called(node.args[1], {'len'}),
len(node.args) in {2, 3}
and isinstance(node.args[1], ast.Call)
and functions.given_function_called(node.args[1], {'len'}),
)

@@ -285,16 +194,19 @@

@final
@decorators.alias('visit_any_function', (
'visit_AsyncFunctionDef',
'visit_FunctionDef',
))
@decorators.alias(
'visit_any_function',
(
'visit_AsyncFunctionDef',
'visit_FunctionDef',
),
)
class FunctionDefinitionVisitor(base.BaseNodeVisitor):
"""Responsible for checking function internals."""
_descriptor_decorators: ClassVar[
FrozenSet[str]
] = frozenset((
'classmethod',
'staticmethod',
'property',
))
_descriptor_decorators: ClassVar[frozenset[str]] = frozenset(
(
'classmethod',
'staticmethod',
'property',
),
)

@@ -309,10 +221,12 @@ def visit_any_function(self, node: AnyFunctionDef) -> None:

def _check_unused_variables(self, node: AnyFunctionDef) -> None:
local_variables: Dict[str, List[_LocalVariable]] = {}
local_variables: dict[str, list[_LocalVariable]] = {}
for body_item in node.body:
for sub_node in ast.walk(body_item):
if isinstance(sub_node, (ast.Name, ast.ExceptHandler)):
if isinstance(sub_node, ast.Name | ast.ExceptHandler):
var_name = variables.get_variable_name(sub_node)
self._maybe_update_variable(
sub_node, var_name, local_variables,
sub_node,
var_name,
local_variables,
)

@@ -351,3 +265,3 @@

var_name: str,
local_variables: Dict[str, List[_LocalVariable]],
local_variables: dict[str, list[_LocalVariable]],
) -> None:

@@ -363,5 +277,5 @@ defs = local_variables.get(var_name)

is_name_def = (
isinstance(sub_node, ast.Name) and
isinstance(sub_node.ctx, ast.Store)
is_name_def = isinstance(sub_node, ast.Name) and isinstance(
sub_node.ctx,
ast.Store,
)

@@ -374,3 +288,3 @@

self,
local_variables: Mapping[str, List[_LocalVariable]],
local_variables: Mapping[str, list[_LocalVariable]],
) -> None:

@@ -382,3 +296,4 @@ for varname, usages in local_variables.items():

naming.UnusedVariableIsUsedViolation(
node, text=varname,
node,
text=varname,
),

@@ -420,3 +335,2 @@ )

# as in the call def, ignoring it.
# `kw_defaults` can have [None, ...] items.
return

@@ -431,7 +345,10 @@

@final
@decorators.alias('visit_any_function_and_lambda', (
'visit_AsyncFunctionDef',
'visit_FunctionDef',
'visit_Lambda',
))
@decorators.alias(
'visit_any_function_and_lambda',
(
'visit_AsyncFunctionDef',
'visit_FunctionDef',
'visit_Lambda',
),
)
class FunctionSignatureVisitor(base.BaseNodeVisitor):

@@ -447,9 +364,6 @@ """

_allowed_default_value_types: ClassVar[AnyNodes] = (
*TextNodes,
ast.Name,
ast.Attribute,
ast.NameConstant,
ast.Tuple,
ast.Num,
ast.Ellipsis,
ast.Constant,
)

@@ -463,2 +377,3 @@

self._check_complex_argument_defaults(node)
self._check_problematic_params(node)
if not isinstance(node, ast.Lambda):

@@ -485,8 +400,2 @@ self._check_getter_without_return(node)

def _is_concrete_getter(self, node: AnyFunctionDef) -> bool:
return (
node.name.startswith('get_') and
not stubs.is_stub(node)
)
def _check_complex_argument_defaults(

@@ -496,40 +405,41 @@ self,

) -> None:
all_defaults = filter(None, (
*node.args.defaults,
*node.args.kw_defaults,
))
all_defaults = filter(
None,
(
*node.args.defaults,
*node.args.kw_defaults,
),
)
for arg in all_defaults:
real_arg = operators.unwrap_unary_node(arg)
parts = attributes.parts(real_arg) if isinstance(
real_arg, ast.Attribute,
) else [real_arg]
parts = (
attributes.parts(real_arg)
if isinstance(real_arg, ast.Attribute)
else [real_arg]
)
has_incorrect_part = any(
if any(
not isinstance(part, self._allowed_default_value_types)
for part in parts
)
if has_incorrect_part:
):
self.add_violation(ComplexDefaultValueViolation(arg))
def _check_problematic_params(
self,
node: AnyFunctionDefAndLambda,
) -> None:
is_problematic = False
if len(node.args.defaults) - len(node.args.args) >= 2:
# This means that we have at least 2 pos-only with defaults.
is_problematic = True
if node.args.defaults and node.args.vararg:
# Won't be able to pass only `*args`,
# will have to pass param before it.
is_problematic = True
@final
class UnnecessaryLiteralsVisitor(base.BaseNodeVisitor):
"""
Responsible for restricting some literals.
if is_problematic:
self.add_violation(ProblematicFunctionParamsViolation(node))
All these literals are defined in ``LITERALS_BLACKLIST``.
"""
def visit_Call(self, node: ast.Call) -> None:
"""Used to find ``LITERALS_BLACKLIST`` without args calls."""
self._check_unnecessary_literals(node)
self.generic_visit(node)
def _check_unnecessary_literals(self, node: ast.Call) -> None:
function_name = functions.given_function_called(
node, LITERALS_BLACKLIST,
)
if function_name and not node.args:
self.add_violation(consistency.UnnecessaryLiteralsViolation(node))
def _is_concrete_getter(self, node: AnyFunctionDef) -> bool:
return node.name.startswith('get_') and not stubs.is_stub(node)
import ast
from collections import defaultdict
from itertools import chain, product
from typing import DefaultDict, Iterable, List, Set
from itertools import product
from typing import Final, TypeAlias, final
from typing_extensions import Final, final
from wemake_python_styleguide.constants import FUTURE_IMPORTS_WHITELIST
from wemake_python_styleguide.logic import nodes
from wemake_python_styleguide.logic.naming import access
from wemake_python_styleguide.logic.tree import imports
from wemake_python_styleguide.types import AnyImport, ConfigurationOptions
from wemake_python_styleguide.options.validation import ValidatedOptions
from wemake_python_styleguide.violations.base import ErrorCallback

@@ -18,5 +15,2 @@ from wemake_python_styleguide.violations.best_practices import (

ImportObjectCollisionViolation,
NestedImportViolation,
ProtectedModuleMemberViolation,
ProtectedModuleViolation,
)

@@ -28,3 +22,2 @@ from wemake_python_styleguide.violations.consistency import (

)
from wemake_python_styleguide.violations.naming import SameAliasImportViolation
from wemake_python_styleguide.visitors.base import BaseNodeVisitor

@@ -35,3 +28,5 @@

_NameAndContext: TypeAlias = tuple[str, ast.AST | None]
class _BaseImportValidator:

@@ -43,3 +38,3 @@ """Base utility class to separate logic from the visitor."""

error_callback: ErrorCallback,
options: ConfigurationOptions,
options: ValidatedOptions,
) -> None:

@@ -49,20 +44,3 @@ self._error_callback = error_callback

def _validate_any_import(self, node: AnyImport) -> None:
self._check_nested_import(node)
self._check_same_alias(node)
def _check_nested_import(self, node: AnyImport) -> None:
parent = nodes.get_parent(node)
if parent is not None and not isinstance(parent, ast.Module):
if not imports.is_nested_typing_import(parent):
self._error_callback(NestedImportViolation(node))
def _check_same_alias(self, node: AnyImport) -> None:
for alias in node.names:
if alias.asname == alias.name and self._options.i_control_code:
self._error_callback(
SameAliasImportViolation(node, text=alias.name),
)
@final

@@ -73,5 +51,3 @@ class _ImportValidator(_BaseImportValidator):

def validate(self, node: ast.Import) -> None:
self._validate_any_import(node)
self._check_dotted_raw_import(node)
self._check_protected_import(node)

@@ -85,12 +61,3 @@ def _check_dotted_raw_import(self, node: ast.Import) -> None:

def _check_protected_import(self, node: ast.Import) -> None:
names: Iterable[str] = chain.from_iterable([
alias.name.split(_MODULE_MEMBERS_SEPARATOR)
for alias in node.names
])
for name in names:
if access.is_protected(name):
self._error_callback(ProtectedModuleViolation(node, text=name))
@final

@@ -101,6 +68,3 @@ class _ImportFromValidator(_BaseImportValidator):

def validate(self, node: ast.ImportFrom) -> None:
self._validate_any_import(node)
self._check_from_import(node)
self._check_protected_import_from_module(node)
self._check_protected_import_from_members(node)
self._check_vague_alias(node)

@@ -119,17 +83,2 @@

def _check_protected_import_from_module(self, node: ast.ImportFrom) -> None:
for name in imports.get_import_parts(node):
if access.is_protected(name):
self._error_callback(ProtectedModuleViolation(node, text=name))
def _check_protected_import_from_members(
self,
node: ast.ImportFrom,
) -> None:
for alias in node.names:
if access.is_protected(alias.name):
self._error_callback(
ProtectedModuleMemberViolation(node, text=alias.name),
)
def _check_vague_alias(self, node: ast.ImportFrom) -> None:

@@ -139,5 +88,4 @@ for alias in node.names:

is_regular_import = (
(alias.asname and name != alias.asname) or
not imports.is_vague_import(name)
)
alias.asname and name != alias.asname
) or not imports.is_vague_import(name)

@@ -159,6 +107,8 @@ if not is_regular_import:

self._error_callback = error_callback
self._imported_names: List[imports.ImportedObjectInfo] = []
self._imported_names: list[imports.ImportedObjectInfo] = []
# This helps us to detect cases like:
# `from x import y, y as z`
self._imported_objects: DefaultDict[str, Set[str]] = defaultdict(set)
self._imported_objects: defaultdict[_NameAndContext, set[str]] = (
defaultdict(set)
)

@@ -175,6 +125,8 @@ def validate(self) -> None:

if self._does_collide(first, second):
self._error_callback(ImportCollisionViolation(
first.node,
second.module,
))
self._error_callback(
ImportCollisionViolation(
first.node,
second.module,
),
)

@@ -185,6 +137,8 @@ def add_import(self, node: ast.Import) -> None:

if not alias.asname:
self._imported_names.append(imports.ImportedObjectInfo(
alias.name,
node,
))
self._imported_names.append(
imports.ImportedObjectInfo(
alias.name,
node,
),
)

@@ -195,16 +149,19 @@ def add_import_from(self, node: ast.ImportFrom) -> None:

identifier = imports.get_module_name(node)
if alias.name in self._imported_objects[identifier]:
context = nodes.get_context(node)
if alias.name in self._imported_objects[identifier, context]:
self._error_callback(
ImportObjectCollisionViolation(node, alias.name),
)
self._imported_objects[identifier].add(alias.name)
self._imported_objects[identifier, context].add(alias.name)
if not alias.asname:
self._imported_names.append(imports.ImportedObjectInfo(
_MODULE_MEMBERS_SEPARATOR.join(
# ignoring `from . import some` case:
filter(None, (node.module, alias.name)),
self._imported_names.append(
imports.ImportedObjectInfo(
_MODULE_MEMBERS_SEPARATOR.join(
# ignoring `from . import some` case:
filter(None, (node.module, alias.name)),
),
node,
),
node,
))
)

@@ -211,0 +168,0 @@ def _does_collide(

import ast
from typing import ClassVar
from typing import ClassVar, final
from typing_extensions import final
from wemake_python_styleguide.logic.nodes import get_parent

@@ -31,4 +29,13 @@ from wemake_python_styleguide.types import AnyNodes

parent = get_parent(node)
if isinstance(parent, self._unpackable_iterable_parent_types):
if len(getattr(parent, 'elts', [])) == 1:
self.add_violation(IterableUnpackingViolation(node))
if not isinstance(parent, self._unpackable_iterable_parent_types):
return
if len(getattr(parent, 'elts', [])) != 1:
return
container = get_parent(parent)
if isinstance(container, ast.Subscript): # pragma: >=3.11 cover
# We ignore cases like `Tuple[*Shape]`, because it is a type
# annotation which should be used like this.
# It is only possible for Python 3.11+
return
self.add_violation(IterableUnpackingViolation(node))
import ast
from typing import ClassVar, Dict, FrozenSet, List, Optional, Type, Union, cast
from typing import ClassVar, TypeAlias, final
from typing_extensions import TypeAlias, final
from wemake_python_styleguide.compat.aliases import (
AssignNodes,
FunctionNodes,
TextNodes,
)
from wemake_python_styleguide.logic import walk, walrus
from wemake_python_styleguide.logic.naming import name_nodes
from wemake_python_styleguide.logic.nodes import get_parent

@@ -22,9 +17,6 @@ from wemake_python_styleguide.logic.tree import keywords, operators

)
from wemake_python_styleguide.types import AnyFunctionDef, AnyNodes, AnyWith
from wemake_python_styleguide.types import AnyFunctionDef, AnyNodes
from wemake_python_styleguide.violations.best_practices import (
BareRaiseViolation,
BaseExceptionRaiseViolation,
ContextManagerVariableDefinitionViolation,
RaiseFromItselfViolation,
RaiseNotImplementedViolation,
WrongKeywordConditionViolation,

@@ -35,7 +27,6 @@ WrongKeywordViolation,

ConsecutiveYieldsViolation,
InconsistentReturnVariableViolation,
InconsistentReturnViolation,
InconsistentYieldViolation,
IncorrectYieldFromTargetViolation,
MultipleContextManagerAssignmentsViolation,
RaiseSystemExitViolation,
)

@@ -46,6 +37,5 @@ from wemake_python_styleguide.visitors.base import BaseNodeVisitor

#: Utility type to work with violations easier.
_ReturningViolations: TypeAlias = Union[
Type[InconsistentReturnViolation],
Type[InconsistentYieldViolation],
]
_ReturningViolations: TypeAlias = (
type[InconsistentReturnViolation] | type[InconsistentYieldViolation]
)

@@ -57,43 +47,29 @@

_base_exceptions: ClassVar[FrozenSet[str]] = frozenset((
'Exception',
'BaseException',
))
_system_error_name: ClassVar[str] = 'SystemExit'
def visit_Raise(self, node: ast.Raise) -> None:
"""Checks how ``raise`` keyword is used."""
self._check_exception_type(node)
self._check_bare_raise(node)
self._check_raise_from_itself(node)
self._check_raise_system_error(node)
self.generic_visit(node)
def _check_exception_type(self, node: ast.Raise) -> None:
exception_name = get_exception_name(node)
if exception_name == 'NotImplemented':
self.add_violation(RaiseNotImplementedViolation(node))
elif exception_name in self._base_exceptions:
self.add_violation(
BaseExceptionRaiseViolation(node, text=exception_name),
)
def _check_bare_raise(self, node: ast.Raise) -> None:
if node.exc is None:
parent_except = walk.get_closest_parent(node, ast.ExceptHandler)
if not parent_except:
self.add_violation(BareRaiseViolation(node))
def _check_raise_from_itself(self, node: ast.Raise) -> None:
if node.exc and node.cause:
names_are_same = get_exception_name(node) == get_cause_name(node)
raising_name = get_exception_name(node)
names_are_same = raising_name == get_cause_name(node)
if raising_name is not None and names_are_same:
self.add_violation(RaiseFromItselfViolation(node))
if names_are_same:
self.add_violation(RaiseFromItselfViolation(node))
def _check_raise_system_error(self, node: ast.Raise) -> None:
if get_exception_name(node) == self._system_error_name:
self.add_violation(RaiseSystemExitViolation(node))
@final
@alias('visit_any_function', (
'visit_FunctionDef',
'visit_AsyncFunctionDef',
))
@alias(
'visit_any_function',
(
'visit_FunctionDef',
'visit_AsyncFunctionDef',
),
)
class ConsistentReturningVisitor(BaseNodeVisitor):

@@ -118,18 +94,22 @@ """Finds incorrect and inconsistent ``return`` and ``yield`` nodes."""

returns = len(tuple(filter(
lambda return_node: return_node.value is not None,
walk.get_subnodes_by_type(parent, ast.Return),
)))
returns = len(
tuple(
filter(
lambda return_node: return_node.value is not None,
walk.get_subnodes_by_type(parent, ast.Return),
),
),
)
last_value_return = (
len(parent.body) > 1 and
returns < 2 and
isinstance(node.value, ast.NameConstant) and
node.value.value is None
len(parent.body) > 1
and returns < 2
and isinstance(node.value, ast.Constant)
and node.value.value is None
)
one_return_with_none = (
returns == 1 and
isinstance(node.value, ast.NameConstant) and
node.value.value is None
returns == 1
and isinstance(node.value, ast.Constant)
and node.value.value is None
)

@@ -143,17 +123,15 @@

node: AnyFunctionDef,
returning_type: Union[Type[ast.Return], Type[ast.Yield]],
returning_type: type[ast.Return] | type[ast.Yield],
violation: _ReturningViolations,
):
return_nodes, has_values = keywords.returning_nodes(
node, returning_type,
node,
returning_type,
)
is_all_none = (
has_values and
all(
(
isinstance(ret_node.value, ast.NameConstant) and
ret_node.value.value is None
)
for ret_node in return_nodes
is_all_none = has_values and all(
(
isinstance(ret_node.value, ast.Constant)
and ret_node.value.value is None
)
for ret_node in return_nodes
)

@@ -169,3 +147,5 @@ if is_all_none:

self._iterate_returning_values(
node, ast.Return, InconsistentReturnViolation,
node,
ast.Return,
InconsistentReturnViolation,
)

@@ -175,3 +155,5 @@

self._iterate_returning_values(
node, ast.Yield, InconsistentYieldViolation,
node,
ast.Yield,
InconsistentYieldViolation,
)

@@ -181,13 +163,15 @@

@final
@alias(
'visit_forbidden_keyword',
(
'visit_Pass',
'visit_Delete',
'visit_Global',
'visit_Nonlocal',
),
)
class WrongKeywordVisitor(BaseNodeVisitor):
"""Finds wrong keywords."""
_forbidden_keywords: ClassVar[AnyNodes] = (
ast.Pass,
ast.Delete,
ast.Global,
ast.Nonlocal,
)
def visit(self, node: ast.AST) -> None:
def visit_forbidden_keyword(self, node: ast.AST) -> None:
"""Used to find wrong keywords."""

@@ -198,16 +182,16 @@ self._check_keyword(node)

def _check_keyword(self, node: ast.AST) -> None:
if isinstance(node, self._forbidden_keywords):
if isinstance(node, ast.Delete):
message = 'del'
else:
message = node.__class__.__qualname__.lower()
if isinstance(node, ast.Pass) and walk.get_closest_parent(
node, ast.match_case
):
return # We allow `pass` in `match: case:`
self.add_violation(WrongKeywordViolation(node, text=message))
if isinstance(node, ast.Delete):
message = 'del'
else:
message = node.__class__.__qualname__.lower()
self.add_violation(WrongKeywordViolation(node, text=message))
@final
@alias('visit_any_with', (
'visit_With',
'visit_AsyncWith',
))
class WrongContextManagerVisitor(BaseNodeVisitor):

@@ -221,13 +205,2 @@ """Checks context managers."""

def visit_any_with(self, node: AnyWith) -> None:
"""Checks the number of assignments for context managers."""
self._check_target_assignment(node)
self.generic_visit(node)
def _check_target_assignment(self, node: AnyWith):
if len(node.items) > 1:
self.add_violation(
MultipleContextManagerAssignmentsViolation(node),
)
def _check_variable_definitions(self, node: ast.withitem) -> None:

@@ -244,6 +217,9 @@ if node.optional_vars is None:

@final
@alias('visit_any_function', (
'visit_FunctionDef',
'visit_AsyncFunctionDef',
))
@alias(
'visit_any_function',
(
'visit_FunctionDef',
'visit_AsyncFunctionDef',
),
)
class GeneratorKeywordsVisitor(BaseNodeVisitor):

@@ -257,3 +233,2 @@ """Checks how generators are defined and used."""

ast.Subscript,
ast.Tuple,

@@ -266,3 +241,3 @@ ast.GeneratorExp,

super().__init__(*args, **kwargs)
self._yield_locations: Dict[int, ast.Expr] = {}
self._yield_locations: dict[int, ast.Expr] = {}

@@ -290,9 +265,8 @@ def visit_any_function(self, node: AnyFunctionDef) -> None:

def _check_yield_from_empty(self, node: ast.YieldFrom) -> None:
if isinstance(node.value, ast.Tuple):
if not node.value.elts:
self.add_violation(IncorrectYieldFromTargetViolation(node))
if isinstance(node.value, ast.Tuple) and not node.value.elts:
self.add_violation(IncorrectYieldFromTargetViolation(node))
def _post_visit(self) -> None:
previous_line: Optional[int] = None
previous_parent: Optional[ast.AST] = None
previous_line: int | None = None
previous_parent: ast.AST | None = None

@@ -302,6 +276,9 @@ for line, node in self._yield_locations.items():

if previous_line is not None:
if line - 1 == previous_line and previous_parent == parent:
self.add_violation(ConsecutiveYieldsViolation(node.value))
break
if (
previous_line is not None
and line - 1 == previous_line
and previous_parent == parent
):
self.add_violation(ConsecutiveYieldsViolation(node.value))
break

@@ -313,68 +290,2 @@ previous_line = line

@final
class ConsistentReturningVariableVisitor(BaseNodeVisitor):
"""Finds variables that are only used in ``return`` statements."""
def visit_Return(self, node: ast.Return) -> None:
"""Helper to get all ``return`` variables in a function at once."""
self._check_consistent_variable_return(node)
self.generic_visit(node)
def _check_consistent_variable_return(self, node: ast.Return) -> None:
if not node.value or not self._is_named_return(node):
return
previous_node = self._get_previous_stmt(node)
if not isinstance(previous_node, AssignNodes):
return
return_names = name_nodes.get_variables_from_node(node.value)
previous_names = list(name_nodes.flat_variable_names([previous_node]))
self._check_for_violations(node, return_names, previous_names)
def _is_named_return(self, node: ast.Return) -> bool:
if isinstance(node.value, ast.Name):
return True
return (
isinstance(node.value, ast.Tuple) and
all(isinstance(elem, ast.Name) for elem in node.value.elts)
)
def _get_previous_stmt(self, node: ast.Return) -> Optional[ast.stmt]:
"""
This method gets the previous node in a block.
It is kind of strange. Because nodes might have several bodies.
Like ``try`` or ``for`` or ``if`` nodes.
``return`` can also be the only statement there.
We also use ``cast`` for a reason.
Because ``return`` always has a parent.
"""
parent = cast(ast.AST, get_parent(node))
for part in ('body', 'orelse', 'finalbody'):
block: List[ast.stmt] = getattr(parent, part, [])
try:
current_index = block.index(node)
except ValueError:
continue
if current_index > 0:
return block[current_index - 1]
return None
def _check_for_violations(
self,
node: ast.Return,
return_names: List[str],
previous_names: List[str],
) -> None:
if previous_names == return_names:
self.add_violation(
InconsistentReturnVariableViolation(
node, text=', '.join(return_names),
),
)
@final
class ConstantKeywordVisitor(BaseNodeVisitor):

@@ -384,4 +295,3 @@ """Visits keyword definitions to detect constant conditions."""

_forbidden_nodes: ClassVar[AnyNodes] = (
ast.NameConstant,
ast.Constant,
ast.List,

@@ -391,3 +301,2 @@ ast.Tuple,

ast.Dict,
ast.ListComp,

@@ -397,6 +306,3 @@ ast.GeneratorExp,

ast.DictComp,
*TextNodes,
ast.Num,
ast.Constant,
ast.IfExp,

@@ -416,5 +322,8 @@ )

def _check_condition(self, node: ast.AST, cond: ast.AST) -> None:
if isinstance(cond, ast.NameConstant) and cond.value is True:
if isinstance(node, ast.While):
return # We should allow plain `while True:`
if (
isinstance(cond, ast.Constant)
and cond.value is True
and isinstance(node, ast.While)
):
return # We should allow plain `while True:`

@@ -421,0 +330,0 @@ real_node = operators.unwrap_unary_node(walrus.get_assigned_expr(cond))

import ast
from collections import defaultdict
from collections.abc import Mapping, Sequence
from contextlib import suppress
from typing import ClassVar, DefaultDict, List, Mapping, Sequence, Type, Union
from typing import ClassVar, TypeAlias, final
from typing_extensions import TypeAlias, final
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.tree import loops, operators, slices
from wemake_python_styleguide.logic import nodes, walk
from wemake_python_styleguide.logic.tree import loops, operators
from wemake_python_styleguide.logic.tree.variables import (

@@ -30,3 +27,2 @@ is_valid_block_variable_definition,

from wemake_python_styleguide.violations.consistency import (
MultilineLoopViolation,
MultipleIfsInComprehensionViolation,

@@ -37,5 +33,3 @@ UselessContinueViolation,

from wemake_python_styleguide.violations.refactoring import (
ImplicitItemsIteratorViolation,
ImplicitSumViolation,
ImplicitYieldFromViolation,
UselessLoopElseViolation,

@@ -46,12 +40,15 @@ )

#: Type alias to specify how we check different containers in loops.
_ContainerSpec: TypeAlias = Mapping[Type[ast.AST], Sequence[str]]
_ContainerSpec: TypeAlias = Mapping[type[ast.AST], Sequence[str]]
@final
@decorators.alias('visit_any_comprehension', (
'visit_ListComp',
'visit_DictComp',
'visit_SetComp',
'visit_GeneratorExp',
))
@decorators.alias(
'visit_any_comprehension',
(
'visit_ListComp',
'visit_DictComp',
'visit_SetComp',
'visit_GeneratorExp',
),
)
class WrongComprehensionVisitor(base.BaseNodeVisitor):

@@ -66,3 +63,3 @@ """Checks comprehensions for correctness."""

super().__init__(*args, **kwargs)
self._fors: DefaultDict[ast.AST, int] = defaultdict(int)
self._fors: defaultdict[ast.AST, int] = defaultdict(int)

@@ -97,13 +94,19 @@ def visit_comprehension(self, node: ast.comprehension) -> None:

@final
@decorators.alias('visit_any_loop', (
'visit_For',
'visit_While',
'visit_AsyncFor',
))
@decorators.alias('visit_any_comp', (
'visit_ListComp',
'visit_SetComp',
'visit_DictComp',
'visit_GeneratorExp',
))
@decorators.alias(
'visit_any_loop',
(
'visit_For',
'visit_While',
'visit_AsyncFor',
),
)
@decorators.alias(
'visit_any_comp',
(
'visit_ListComp',
'visit_SetComp',
'visit_DictComp',
'visit_GeneratorExp',
),
)
class WrongLoopVisitor(base.BaseNodeVisitor):

@@ -125,3 +128,2 @@ """Responsible for examining loops."""

ast.DictComp: ['key', 'value'],
ast.For: ['body'],

@@ -142,3 +144,2 @@ ast.AsyncFor: ['body'],

self._check_useless_continue(node)
self._check_multiline_loop(node)
self._check_infinite_while_loop(node)

@@ -153,3 +154,3 @@ self.generic_visit(node)

self,
node: Union[AnyLoop, AnyComprehension],
node: AnyLoop | AnyComprehension,
) -> None:

@@ -159,10 +160,4 @@ for lambda_node in walk.get_subnodes_by_type(node, ast.Lambda):

body_nodes = walk.get_subnodes_by_type(lambda_node.body, ast.Name)
arguments = (
arg.arg
for arg in arg_nodes
)
body = (
subnode.id
for subnode in body_nodes
)
arguments = (arg.arg for arg in arg_nodes)
body = (subnode.id for subnode in body_nodes)
if not all(symbol in arguments for symbol in body):

@@ -172,3 +167,3 @@ self.add_violation(LambdaInsideLoopViolation(node))

def _check_useless_continue(self, node: AnyLoop) -> None:
nodes_at_line: DefaultDict[int, List[ast.AST]] = defaultdict(list)
nodes_at_line: defaultdict[int, list[ast.AST]] = defaultdict(list)
for sub_node in ast.walk(node):

@@ -179,16 +174,6 @@ lineno = getattr(sub_node, 'lineno', None)

last_line = nodes_at_line[sorted(nodes_at_line.keys())[-1]]
last_line = nodes_at_line[max(nodes_at_line.keys())]
if any(isinstance(last, ast.Continue) for last in last_line):
self.add_violation(UselessContinueViolation(node))
def _check_multiline_loop(self, node: AnyLoop) -> None:
start_lineno = getattr(node, 'lineno', None)
node_to_check = node.test if isinstance(node, ast.While) else node.iter
for sub_node in ast.walk(node_to_check):
sub_lineno = getattr(sub_node, 'lineno', None)
if sub_lineno is not None and sub_lineno > start_lineno:
self.add_violation(MultilineLoopViolation(node))
break
def _check_infinite_while_loop(self, node: AnyLoop) -> None:

@@ -208,6 +193,9 @@ if not isinstance(node, ast.While):

@final
@decorators.alias('visit_any_for', (
'visit_For',
'visit_AsyncFor',
))
@decorators.alias(
'visit_any_for',
(
'visit_For',
'visit_AsyncFor',
),
)
class WrongLoopDefinitionVisitor(base.BaseNodeVisitor):

@@ -224,4 +212,2 @@ """Responsible for ``for`` loops and comprehensions definitions."""

ast.GeneratorExp,
ast.Num,
ast.NameConstant,
)

@@ -234,3 +220,2 @@

self._check_implicit_sum(node)
self._check_implicit_yield_from(node)
self.generic_visit(node)

@@ -250,3 +235,3 @@

self,
node: Union[AnyFor, ast.comprehension],
node: AnyFor | ast.comprehension,
) -> None:

@@ -261,51 +246,8 @@ node_iter = operators.unwrap_unary_node(node.iter)

is_implicit_sum = (
len(node.body) == 1 and
isinstance(node.body[0], ast.AugAssign) and
isinstance(node.body[0].op, ast.Add) and
isinstance(node.body[0].target, ast.Name)
len(node.body) == 1
and isinstance(node.body[0], ast.AugAssign)
and isinstance(node.body[0].op, ast.Add)
and isinstance(node.body[0].target, ast.Name)
)
if is_implicit_sum:
self.add_violation(ImplicitSumViolation(node))
def _check_implicit_yield_from(self, node: AnyFor) -> None:
if isinstance(nodes.get_context(node), ast.AsyncFunctionDef):
# Python does not support 'yield from' inside async functions
return
is_implicit_yield_from = (
len(node.body) == 1 and
isinstance(node.body[0], ast.Expr) and
isinstance(node.body[0].value, ast.Yield)
)
if is_implicit_yield_from:
self.add_violation(ImplicitYieldFromViolation(node))
@final
class SyncForLoopVisitor(base.BaseNodeVisitor):
"""We use this visitor to check just sync ``for`` loops."""
def visit_For(self, node: ast.For) -> None:
"""Checks for hidden patterns in sync loops."""
self._check_implicit_items(node)
self.generic_visit(node)
def _check_implicit_items(self, node: ast.For) -> None:
iterable = source.node_to_string(node.iter)
target = source.node_to_string(node.target)
for sub in ast.walk(node):
has_violation = (
isinstance(sub, ast.Subscript) and
not self._is_assigned_target(sub) and
slices.is_same_slice(iterable, target, sub)
)
if has_violation:
self.add_violation(ImplicitItemsIteratorViolation(node))
break
def _is_assigned_target(self, node: ast.Subscript) -> bool:
parent = nodes.get_parent(node)
if not isinstance(parent, (*AssignNodes, ast.AugAssign)):
return False
return any(node == target for target in get_assign_targets(parent))
import ast
from typing import ClassVar, Iterable, cast
from collections.abc import Iterable
from typing import ClassVar, cast, final
from typing_extensions import final
from wemake_python_styleguide import constants

@@ -52,5 +51,2 @@ from wemake_python_styleguide.logic.filenames import get_stem

if not self.options.i_control_code:
return
if len(node.body) > 1:

@@ -78,17 +74,19 @@ self.add_violation(InitModuleHasLogicViolation())

def _check_magic_module_functions(self, node: ast.FunctionDef) -> None:
if self.options.i_control_code:
if not isinstance(get_context(node), ast.Module):
return
if not isinstance(get_context(node), ast.Module):
return
if node.name in constants.MAGIC_MODULE_NAMES_BLACKLIST:
self.add_violation(
BadMagicModuleFunctionViolation(node, text=node.name),
)
if node.name in constants.MAGIC_MODULE_NAMES_BLACKLIST:
self.add_violation(
BadMagicModuleFunctionViolation(node, text=node.name),
)
@final
@alias('visit_any_assign', (
'visit_Assign',
'visit_AnnAssign',
))
@alias(
'visit_any_assign',
(
'visit_Assign',
'visit_AnnAssign',
),
)
class ModuleConstantsVisitor(BaseNodeVisitor):

@@ -95,0 +93,0 @@ """Finds incorrect module constants."""

import ast
from typing import Callable, ClassVar, Iterable, Optional, Type
from collections.abc import Callable, Iterable
from typing import ClassVar, TypeAlias, final
import attr
from typing_extensions import final

@@ -29,4 +29,4 @@ from wemake_python_styleguide.compat.functions import (

functions,
variables,
)
from wemake_python_styleguide.options.validation import ValidatedOptions
from wemake_python_styleguide.types import (

@@ -37,3 +37,2 @@ AnyAssign,

AnyVariableDef,
ConfigurationOptions,
)

@@ -44,6 +43,6 @@ from wemake_python_styleguide.violations import base, naming

_ErrorCallback = Callable[[base.BaseViolation], None]
_ErrorCallback: TypeAlias = Callable[[base.BaseViolation], None]
_PredicateApplicableCallback = Callable[[ast.AST], bool]
_PredicateLogicalCallback = Callable[[str], bool]
_PredicateApplicableCallback: TypeAlias = Callable[[ast.AST], bool]
_PredicateLogicalCallback: TypeAlias = Callable[[str], bool]

@@ -57,5 +56,5 @@

is_correct: _PredicateLogicalCallback
violation: Type[base.BaseViolation]
violation: type[base.BaseViolation]
_is_applicable: Optional[_PredicateApplicableCallback] = None
_is_applicable: _PredicateApplicableCallback | None = None

@@ -76,6 +75,2 @@ def is_applicable(self, node: ast.AST) -> bool:

_NamingPredicate(
alphabet.does_contain_unicode,
naming.UnicodeNameViolation,
),
_NamingPredicate(
lambda name: access.is_unused(name) and len(name) > 1,

@@ -89,3 +84,3 @@ naming.WrongUnusedVariableNameViolation,

error_callback: _ErrorCallback,
options: ConfigurationOptions,
options: ValidatedOptions,
) -> None:

@@ -111,3 +106,5 @@ """Creates new instance of a name validator."""

self._ensure_reserved_name(
node, name, is_first_argument=is_first_argument,
node,
name,
is_first_argument=is_first_argument,
)

@@ -135,14 +132,7 @@

_naming_predicates: ClassVar[Iterable[_NamingPredicate]] = (
*_SimpleNameValidator._naming_predicates, # noqa: WPS437
*_SimpleNameValidator._naming_predicates, # noqa: SLF001
_NamingPredicate(
builtins.is_builtin_name,
naming.BuiltinShadowingViolation,
variables.does_shadow_builtin,
),
_NamingPredicate(
builtins.is_wrong_alias,
naming.TrailingUnderscoreViolation,
),
_NamingPredicate(

@@ -172,15 +162,26 @@ alphabet.does_contain_underscored_number,

def _ensure_length(self, node: ast.AST, name: str) -> None:
min_length = self._options.min_name_length
if logical.is_too_short_name(name, min_length=min_length):
if (
logical.is_too_short_name(
name,
min_length=self._options.min_name_length,
)
and name not in self._options.allowed_domain_names
):
self._error_callback(
naming.TooShortNameViolation(
node, text=name, baseline=min_length,
node,
text=name,
baseline=self._options.min_name_length,
),
)
max_length = self._options.max_name_length
if logical.is_too_long_name(name, max_length=max_length):
if logical.is_too_long_name(
name,
max_length=self._options.max_name_length,
):
self._error_callback(
naming.TooLongNameViolation(
node, text=name, baseline=max_length,
node,
text=name,
baseline=self._options.max_name_length,
),

@@ -197,3 +198,4 @@ )

unreadable_sequence = alphabet.get_unreadable_characters(
name, UNREADABLE_CHARACTER_COMBINATIONS,
name,
UNREADABLE_CHARACTER_COMBINATIONS,
)

@@ -210,8 +212,10 @@ if unreadable_sequence:

for arg in functions.get_all_arguments(node):
is_first_argument = (
functions.is_first_argument(node, arg.arg) and
not isinstance(node, ast.Lambda)
)
is_first_argument = functions.is_first_argument(
node,
arg.arg,
) and not isinstance(node, ast.Lambda)
self.check_name(
arg, arg.arg, is_first_argument=is_first_argument,
arg,
arg.arg,
is_first_argument=is_first_argument,
)

@@ -222,3 +226,3 @@

class _TypeParamNameValidator(_RegularNameValidator):
def check_type_params( # pragma: py-lt-312
def check_type_params( # pragma: >=3.12 cover
self,

@@ -235,3 +239,4 @@ node: NodeWithTypeParams,

class_attributes, _ = classes.get_attributes(
node, include_annotated=True,
node,
include_annotated=True,
)

@@ -252,20 +257,32 @@

@final
@alias('visit_any_import', (
'visit_ImportFrom',
'visit_Import',
))
@alias('visit_variable', (
'visit_Name',
'visit_Attribute',
'visit_ExceptHandler',
))
@alias('visit_any_function', (
'visit_FunctionDef',
'visit_AsyncFunctionDef',
'visit_Lambda',
))
@alias('visit_named_match', (
'visit_MatchStar',
'visit_MatchAs',
))
@alias(
'visit_any_import',
(
'visit_ImportFrom',
'visit_Import',
),
)
@alias(
'visit_variable',
(
'visit_Name',
'visit_Attribute',
'visit_ExceptHandler',
),
)
@alias(
'visit_any_function',
(
'visit_FunctionDef',
'visit_AsyncFunctionDef',
'visit_Lambda',
),
)
@alias(
'visit_named_match',
(
'visit_MatchStar',
'visit_MatchAs',
),
)
class WrongNameVisitor(BaseNodeVisitor):

@@ -278,15 +295,20 @@ """Performs checks based on variable names."""

self._simple_validator = _SimpleNameValidator(
self.add_violation, self.options,
self.add_violation,
self.options,
)
self._regular_validator = _RegularNameValidator(
self.add_violation, self.options,
self.add_violation,
self.options,
)
self._function_validator = _FunctionNameValidator(
self.add_violation, self.options,
self.add_violation,
self.options,
)
self._class_based_validator = _ClassBasedNameValidator(
self.add_violation, self.options,
self.add_violation,
self.options,
)
self._type_params_validator = _TypeParamNameValidator(
self.add_violation, self.options,
self.add_violation,
self.options,
)

@@ -303,5 +325,9 @@

"""Used to check wrong names of assigned."""
validator = self._simple_validator if attributes.is_foreign_attribute(
node,
) else self._regular_validator
validator = (
self._simple_validator
if attributes.is_foreign_attribute(
node,
)
else self._regular_validator
)

@@ -328,3 +354,3 @@ variable_name = name_nodes.get_assigned_name(node)

def visit_named_match(self, node: NamedMatch) -> None: # pragma: py-lt-310
def visit_named_match(self, node: NamedMatch) -> None:
"""

@@ -343,5 +369,8 @@ Check pattern matching.

def visit_TypeAlias(self, node: TypeAliasNode) -> None: # pragma: py-lt-312
def visit_TypeAlias(
self,
node: TypeAliasNode,
) -> None: # pragma: >=3.12 cover
"""Visit PEP695 type aliases."""
self._type_params_validator.check_type_params(node)
self.generic_visit(node)
import ast
import itertools
from collections import Counter
from typing import Iterable, List, cast
from collections.abc import Iterable
from typing import cast, final
from typing_extensions import final
from wemake_python_styleguide.compat.functions import get_assign_targets
from wemake_python_styleguide.compat.nodes import Match
from wemake_python_styleguide.constants import (
MODULE_METADATA_VARIABLES_BLACKLIST,
UNUSED_PLACEHOLDER,
)
from wemake_python_styleguide.logic import nodes
from wemake_python_styleguide.logic.naming import access, name_nodes
from wemake_python_styleguide.logic.naming import access, blacklists, name_nodes
from wemake_python_styleguide.logic.tree import pattern_matching

@@ -28,6 +23,9 @@ from wemake_python_styleguide.types import (

@final
@alias('visit_any_assign', (
'visit_Assign',
'visit_AnnAssign',
))
@alias(
'visit_any_assign',
(
'visit_Assign',
'visit_AnnAssign',
),
)
class WrongModuleMetadataVisitor(BaseNodeVisitor):

@@ -50,3 +48,5 @@ """Finds wrong metadata information of a module."""

if target_node.id not in MODULE_METADATA_VARIABLES_BLACKLIST:
if target_node.id not in blacklists.module_metadata_blacklist(
self.options,
):
continue

@@ -56,3 +56,4 @@

best_practices.WrongModuleMetadataViolation(
node, text=target_node.id,
node,
text=target_node.id,
),

@@ -63,80 +64,17 @@ )

@final
@alias('visit_any_assign', (
'visit_Assign',
'visit_AnnAssign',
))
class WrongVariableAssignmentVisitor(BaseNodeVisitor):
"""Finds wrong variables assignments."""
def visit_any_assign(self, node: AnyAssign) -> None:
"""Used to check assignment variable to itself."""
names = list(name_nodes.flat_variable_names([node]))
self._check_reassignment(node, names)
self._check_unique_assignment(node, names)
self.generic_visit(node)
def _check_reassignment(
self,
node: AnyAssign,
names: List[str],
) -> None:
if not node.value:
return
if self._is_reassignment_edge_case(node):
return
var_values = name_nodes.get_variables_from_node(node.value)
if len(names) <= 1 < len(var_values):
# It means that we have something like `x = (y, z)`
# or even `x = (x, y)`, which is also fine. See #1807
return
for var_name, var_value in itertools.zip_longest(names, var_values):
if var_name == var_value:
self.add_violation(
best_practices.ReassigningVariableToItselfViolation(
node, text=var_name,
),
)
def _check_unique_assignment(
self,
node: AnyAssign,
names: List[str],
) -> None:
used_names = filter(
lambda assigned_name: not access.is_unused(assigned_name),
names,
)
for used_name, count in Counter(used_names).items():
if count > 1:
self.add_violation(
best_practices.ReassigningVariableToItselfViolation(
node, text=used_name,
),
)
def _is_reassignment_edge_case(self, node: AnyAssign) -> bool:
# This is not a variable, but a class property
if isinstance(nodes.get_context(node), ast.ClassDef):
return True
# It means that someone probably modifies original value
# of the variable using some unary operation, e.g. `a = not a`
# See #2189
return isinstance(node.value, ast.UnaryOp)
@final
@alias('visit_any_assign', (
'visit_Assign',
'visit_AnnAssign',
'visit_NamedExpr',
))
@alias('visit_any_for', (
'visit_For',
'visit_AsyncFor',
))
@alias(
'visit_any_assign',
(
'visit_Assign',
'visit_AnnAssign',
'visit_NamedExpr',
),
)
@alias(
'visit_any_for',
(
'visit_For',
'visit_AsyncFor',
),
)
class UnusedVariableDefinitionVisitor(BaseNodeVisitor):

@@ -155,3 +93,3 @@ """Checks how variables are used."""

nodes.get_context(node),
(ast.ClassDef, ast.Module),
ast.ClassDef | ast.Module,
)

@@ -168,4 +106,4 @@ self._check_assign_unused(

target_names = name_nodes.get_variables_from_node(node.target)
is_target_no_op_variable = (
len(target_names) == 1 and access.is_unused(target_names[0])
is_target_no_op_variable = len(target_names) == 1 and access.is_unused(
target_names[0],
)

@@ -196,3 +134,3 @@ if not is_target_no_op_variable: # see issue 1406

def visit_Match(self, node: Match) -> None: # pragma: py-lt-310
def visit_Match(self, node: ast.Match) -> None:
"""Check pattern matching in a form of `case ... as NAME`."""

@@ -223,3 +161,4 @@ for match_as in pattern_matching.get_explicit_as_names(node):

naming.UnusedVariableIsDefinedViolation(
node, text=', '.join(all_names),
node,
text=', '.join(all_names),
),

@@ -236,3 +175,5 @@ )

self._check_variable_used(
node, node.id, is_created=isinstance(node.ctx, ast.Store),
node,
node.id,
is_created=isinstance(node.ctx, ast.Store),
)

@@ -239,0 +180,0 @@ self.generic_visit(node)

import ast
from typing import ClassVar, Mapping, Optional, Tuple, Type, Union
from collections.abc import Mapping
from typing import ClassVar, TypeAlias, final
from typing_extensions import TypeAlias, final
from wemake_python_styleguide.compat.aliases import TextNodes
from wemake_python_styleguide.logic import walk
from wemake_python_styleguide.logic.tree.annotations import is_annotation
from wemake_python_styleguide.logic.tree.operators import (

@@ -16,3 +14,2 @@ count_unary_operator,

from wemake_python_styleguide.violations.best_practices import (
BitwiseAndBooleanMixupViolation,
ListMultiplyViolation,

@@ -24,13 +21,15 @@ )

complex,
Tuple[Type[ast.operator], ...],
tuple[type[ast.operator], ...],
]
_OperatorLimits: TypeAlias = Mapping[Type[ast.unaryop], int]
_NumbersAndConstants: TypeAlias = Union[ast.Num, ast.NameConstant]
_OperatorLimits: TypeAlias = Mapping[type[ast.unaryop], int]
@final
@decorators.alias('visit_numbers_and_constants', (
'visit_Num',
'visit_NameConstant',
))
@decorators.alias(
'visit_numbers_and_constants',
(
'visit_Num',
'visit_NameConstant',
),
)
class UselessOperatorsVisitor(base.BaseNodeVisitor):

@@ -54,3 +53,2 @@ """Checks operators used in the code."""

ast.Pow,
ast.BitAnd,

@@ -83,3 +81,3 @@ ast.BitOr,

def visit_numbers_and_constants(self, node: _NumbersAndConstants) -> None:
def visit_numbers_and_constants(self, node: ast.Constant) -> None:
"""Checks numbers unnecessary operators inside the code."""

@@ -101,8 +99,10 @@ self._check_operator_count(node)

def _check_operator_count(self, node: _NumbersAndConstants) -> None:
def _check_operator_count(self, node: ast.Constant) -> None:
for node_type, limit in self._unary_limits.items():
if count_unary_operator(node, node_type) > limit:
text = str(node.n) if isinstance(node, ast.Num) else node.value
self.add_violation(
consistency.UselessOperatorsViolation(node, text=text),
consistency.UselessOperatorsViolation(
node,
text=str(node.value),
),
)

@@ -114,5 +114,5 @@

is_zero_division = (
isinstance(op, self._zero_divisors) and
isinstance(number, ast.Num) and
number.n == 0
isinstance(op, self._zero_divisors)
and isinstance(number, ast.Constant)
and number.value == 0
)

@@ -125,8 +125,12 @@ if is_zero_division:

op: ast.operator,
left: Optional[ast.AST],
right: Optional[ast.AST] = None,
left: ast.AST | None,
right: ast.AST | None = None,
) -> None:
if isinstance(left, ast.Num) and left.n in self._left_special_cases:
if right and isinstance(op, self._left_special_cases[left.n]):
left = None
if (
isinstance(left, ast.Constant)
and left.value in self._left_special_cases
and right
and isinstance(op, self._left_special_cases[left.value])
):
left = None

@@ -136,3 +140,3 @@ non_negative_numbers = self._get_non_negative_nodes(left, right)

for number in non_negative_numbers:
forbidden = self._meaningless_operations.get(number.n, None)
forbidden = self._meaningless_operations.get(number.value, None)
if forbidden and isinstance(op, forbidden):

@@ -145,14 +149,15 @@ self.add_violation(

self,
left: Optional[ast.AST],
right: Optional[ast.AST] = None,
):
non_negative_numbers = []
left: ast.AST | None,
right: ast.AST | None = None,
) -> list[ast.Constant]:
non_negative_numbers: list[ast.Constant] = []
for node in filter(None, (left, right)):
real_node = unwrap_unary_node(node)
correct_node = (
isinstance(real_node, ast.Num) and
real_node.n in self._meaningless_operations and
not (real_node.n == 1 and walk.is_contained(node, ast.USub))
)
if correct_node:
if (
isinstance(real_node, ast.Constant)
and real_node.value in self._meaningless_operations
and not (
real_node.value == 1 and walk.is_contained(node, ast.USub)
)
):
non_negative_numbers.append(real_node)

@@ -167,3 +172,3 @@ return non_negative_numbers

_string_nodes: ClassVar[AnyNodes] = (
*TextNodes,
TextNodes,
ast.JoinedStr,

@@ -192,5 +197,5 @@ )

is_double_minus = (
isinstance(op, (ast.Add, ast.Sub)) and
isinstance(right, ast.UnaryOp) and
isinstance(right.op, ast.USub)
isinstance(op, ast.Add | ast.Sub)
and isinstance(right, ast.UnaryOp)
and isinstance(right.op, ast.USub)
)

@@ -203,5 +208,5 @@ if is_double_minus:

def _check_list_multiply(self, node: ast.BinOp) -> None:
is_list_multiply = (
isinstance(node.op, ast.Mult) and
isinstance(node.left, self._list_nodes)
is_list_multiply = isinstance(node.op, ast.Mult) and isinstance(
node.left,
self._list_nodes,
)

@@ -215,3 +220,3 @@ if is_list_multiply:

op: ast.operator,
right: Optional[ast.AST] = None,
right: ast.AST | None = None,
) -> None:

@@ -239,2 +244,9 @@ if not isinstance(op, ast.Add):

_available_parents: ClassVar[AnyNodes] = (
ast.ListComp,
ast.SetComp,
ast.DictComp,
ast.GeneratorExp,
)
def visit_NamedExpr(

@@ -244,35 +256,14 @@ self,

) -> None:
"""Disallows walrus ``:=`` operator."""
self.add_violation(consistency.WalrusViolation(node))
"""Disallows walrus ``:=`` operator outside comprehensions."""
self._check_walrus_in_comprehesion(node)
self.generic_visit(node)
@final
class BitwiseOpVisitor(base.BaseNodeVisitor):
"""Checks bitwise operations are used correctly."""
_invalid_nodes: ClassVar[AnyNodes] = (
ast.BoolOp,
ast.UnaryOp,
ast.NameConstant,
ast.Compare,
)
def visit_BinOp(self, node: ast.BinOp) -> None:
"""Finds bad usage of bitwise operation with binary operation."""
self._check_logical_bitwise_operator(node)
self.generic_visit(node)
def _check_logical_bitwise_operator(self, node: ast.BinOp) -> None:
if not isinstance(node.op, (ast.BitOr, ast.BitAnd)):
def _check_walrus_in_comprehesion(
self,
node: ast.NamedExpr,
) -> None:
is_comprension = walk.get_closest_parent(node, self._available_parents)
if is_comprension:
return
if isinstance(node.op, ast.BitOr) and is_annotation(node):
return # We allow new styled union types like: `int | None`
if self._is_bool_like(node.left) or self._is_bool_like(node.right):
self.add_violation(BitwiseAndBooleanMixupViolation(node))
def _is_bool_like(self, node: ast.expr) -> bool:
"""Checks either side of the Bitwise operation invalid usage."""
return isinstance(node, self._invalid_nodes)
self.add_violation(consistency.WalrusViolation(node))
import ast
from typing import Union
from typing import final
from typing_extensions import final
from wemake_python_styleguide.types import AnyComprehension, AnyFor

@@ -15,12 +13,18 @@ from wemake_python_styleguide.violations.best_practices import (

@final
@decorators.alias('visit_any_for', (
'visit_For',
'visit_AsyncFor',
))
@decorators.alias('visit_any_comprehension', (
'visit_ListComp',
'visit_DictComp',
'visit_SetComp',
'visit_GeneratorExp',
))
@decorators.alias(
'visit_any_for',
(
'visit_For',
'visit_AsyncFor',
),
)
@decorators.alias(
'visit_any_comprehension',
(
'visit_ListComp',
'visit_DictComp',
'visit_SetComp',
'visit_GeneratorExp',
),
)
class RedundantEnumerateVisitor(BaseNodeVisitor):

@@ -40,3 +44,6 @@ """Responsible for detecting redundant usages of ``enumerate`` function."""

def _check_for_redundant_enumerate(self, node: Union[AnyFor, ast.comprehension]) -> None: # noqa: E501
def _check_for_redundant_enumerate(
self,
node: AnyFor | ast.comprehension,
) -> None:
if not isinstance(node.iter, ast.Call):

@@ -43,0 +50,0 @@ return

import ast
from typing import ClassVar, Mapping, Optional, Sequence, Set, Union
from collections.abc import Mapping, Sequence
from typing import ClassVar, TypeAlias, final
from typing_extensions import TypeAlias, final
from wemake_python_styleguide import constants
from wemake_python_styleguide import constants, types
from wemake_python_styleguide.compat.aliases import (
ForNodes,
FunctionNodes,
TextNodes,
)
from wemake_python_styleguide.compat.nodes import TryStar
from wemake_python_styleguide.logic import nodes
from wemake_python_styleguide.logic.arguments import call_args
from wemake_python_styleguide.logic.naming import name_nodes
from wemake_python_styleguide.logic.tree import functions, strings
from wemake_python_styleguide.logic.tree.collections import (
first,
normalize_dict_elements,
sequence_of_node,
)
from wemake_python_styleguide.types import (
AnyFor,
AnyFunctionDef,
AnyNodes,
AnyWith,
)
from wemake_python_styleguide.violations.best_practices import (
StatementHasNoEffectViolation,
UnreachableCodeViolation,

@@ -35,3 +23,2 @@ WrongNamedKeywordViolation,

AugmentedAssignPatternViolation,
ParametersIndentationViolation,
UselessNodeViolation,

@@ -49,40 +36,37 @@ )

#: Statements that do have `.body` attribute.
_StatementWithBody: TypeAlias = Union[
ast.If,
AnyFor,
ast.While,
AnyWith,
ast.Try,
TryStar,
ast.ExceptHandler,
AnyFunctionDef,
ast.ClassDef,
ast.Module,
]
_StatementWithBody: TypeAlias = (
ast.If
| types.AnyFor
| ast.While
| types.AnyWith
| ast.Try
| TryStar
| ast.ExceptHandler
| types.AnyFunctionDef
| ast.ClassDef
| ast.Module
| ast.match_case
)
#: Simple collections.
_AnyCollection: TypeAlias = Union[
ast.List,
ast.Set,
ast.Dict,
ast.Tuple,
]
@final
@alias('visit_statement_with_body', (
'visit_If',
'visit_For',
'visit_AsyncFor',
'visit_While',
'visit_With',
'visit_AsyncWith',
'visit_Try',
'visit_TryStar',
'visit_ExceptHandler',
'visit_FunctionDef',
'visit_AsyncFunctionDef',
'visit_ClassDef',
'visit_Module',
))
@alias(
'visit_statement_with_body',
(
'visit_If',
'visit_For',
'visit_AsyncFor',
'visit_While',
'visit_With',
'visit_AsyncWith',
'visit_Try',
'visit_TryStar',
'visit_ExceptHandler',
'visit_FunctionDef',
'visit_AsyncFunctionDef',
'visit_ClassDef',
'visit_Module',
'visit_match_case',
),
)
class StatementsWithBodiesVisitor(BaseNodeVisitor):

@@ -95,3 +79,3 @@ """

_closing_nodes: ClassVar[AnyNodes] = (
_closing_nodes: ClassVar[types.AnyNodes] = (
ast.Raise,

@@ -103,12 +87,4 @@ ast.Return,

_have_doc_strings: ClassVar[AnyNodes] = (
*FunctionNodes,
ast.ClassDef,
ast.Module,
)
_blocked_self_assignment: ClassVar[types.AnyNodes] = (ast.BinOp,)
_blocked_self_assignment: ClassVar[AnyNodes] = (
ast.BinOp,
)
_nodes_with_orelse = (

@@ -122,30 +98,14 @@ ast.If,

_have_effect: ClassVar[AnyNodes] = (
ast.Return,
ast.YieldFrom,
ast.Yield,
ast.Raise,
ast.Break,
ast.Continue,
ast.Call,
ast.Await,
ast.Nonlocal,
ast.Global,
ast.Delete,
ast.Pass,
ast.Assert,
)
# Useless nodes:
_generally_useless_body: ClassVar[AnyNodes] = (
_generally_useless_body: ClassVar[types.AnyNodes] = (
ast.Break,
ast.Continue,
ast.Pass,
ast.Ellipsis,
ast.Constant,
ast.Dict,
ast.List,
ast.Tuple,
ast.Set,
)
_loop_useless_body: ClassVar[AnyNodes] = (
_loop_useless_body: ClassVar[types.AnyNodes] = (
ast.Return,

@@ -155,8 +115,8 @@ ast.Raise,

_useless_combination: ClassVar[Mapping[str, AnyNodes]] = {
_useless_combination: ClassVar[Mapping[str, types.AnyNodes]] = {
'For': _generally_useless_body + _loop_useless_body,
'AsyncFor': _generally_useless_body + _loop_useless_body,
'While': _generally_useless_body + _loop_useless_body,
'Try': _generally_useless_body + (ast.Raise,),
'TryStar': _generally_useless_body + (ast.Raise,),
'Try': (*_generally_useless_body, ast.Raise),
'TryStar': (*_generally_useless_body, ast.Raise),
'With': _generally_useless_body,

@@ -186,3 +146,3 @@ 'AsyncWith': _generally_useless_body,

def _almost_swapped(self, assigns: Sequence[ast.Assign]) -> None:
previous_var: Set[Optional[str]] = set()
previous_var: set[str | None] = set()

@@ -215,3 +175,4 @@ for assign in assigns:

forbidden = self._useless_combination.get(
node.__class__.__qualname__, None,
node.__class__.__qualname__,
None,
)

@@ -224,31 +185,7 @@

UselessNodeViolation(
node, text=node.__class__.__qualname__.lower(),
node,
text=node.__class__.__qualname__.lower(),
),
)
def _check_expression(
self,
node: ast.Expr,
*,
is_first: bool = False,
) -> None:
if isinstance(node.value, self._have_effect):
return
if is_first and strings.is_doc_string(node):
if isinstance(nodes.get_parent(node), self._have_doc_strings):
return
parent = nodes.get_parent(node)
is_only_ellipsis_node = (
isinstance(node.value, ast.Constant) and
node.value.value is Ellipsis and
isinstance(parent, (*FunctionNodes, ast.ClassDef)) and
len(parent.body) == 1
)
if is_only_ellipsis_node:
return
self.add_violation(StatementHasNoEffectViolation(node))
def _check_self_misrefactored_assignment(

@@ -262,9 +199,11 @@ self,

if isinstance(node.value, self._blocked_self_assignment):
if name_nodes.is_same_variable(node.target, node_value):
self.add_violation(MisrefactoredAssignmentViolation(node))
if isinstance(
node.value,
self._blocked_self_assignment,
) and name_nodes.is_same_variable(node.target, node_value):
self.add_violation(MisrefactoredAssignmentViolation(node))
def _check_internals(self, body: Sequence[ast.stmt]) -> None:
after_closing_node = False
for index, statement in enumerate(body):
for statement in body:
if after_closing_node:

@@ -275,4 +214,2 @@ self.add_violation(UnreachableCodeViolation(statement))

after_closing_node = True
elif isinstance(statement, ast.Expr):
self._check_expression(statement, is_first=index == 0)
elif isinstance(statement, ast.AugAssign):

@@ -283,98 +220,6 @@ self._check_self_misrefactored_assignment(statement)

@final
@alias('visit_collection', (
'visit_List',
'visit_Set',
'visit_Dict',
'visit_Tuple',
))
@alias('visit_any_function', (
'visit_FunctionDef',
'visit_AsyncFunctionDef',
))
class WrongParametersIndentationVisitor(BaseNodeVisitor):
"""Ensures that all parameters indentation follow our rules."""
def visit_collection(self, node: _AnyCollection) -> None:
"""Checks how collection items indentation."""
if isinstance(node, ast.Dict):
elements = normalize_dict_elements(node)
else:
elements = node.elts
self._check_indentation(node, elements, extra_lines=1)
self.generic_visit(node)
def visit_Call(self, node: ast.Call) -> None:
"""Checks call arguments indentation."""
all_args = call_args.get_all_args(node)
self._check_indentation(node, all_args)
self.generic_visit(node)
def visit_any_function(self, node: AnyFunctionDef) -> None:
"""Checks function parameters indentation."""
self._check_indentation(node, functions.get_all_arguments(node))
self.generic_visit(node)
def visit_ClassDef(self, node: ast.ClassDef) -> None:
"""Checks base classes indentation."""
all_args = [*node.bases, *[kw.value for kw in node.keywords]]
self._check_indentation(node, all_args)
self.generic_visit(node)
def _check_first_element(
self,
node: ast.AST,
statement: ast.AST,
extra_lines: int,
) -> Optional[bool]:
if statement.lineno == node.lineno and not extra_lines:
return False
return None
def _check_rest_elements(
self,
node: ast.AST,
statement: ast.AST,
previous_line: int,
multi_line_mode: Optional[bool],
) -> Optional[bool]:
previous_has_break = previous_line != statement.lineno
if not previous_has_break and multi_line_mode:
self.add_violation(ParametersIndentationViolation(node))
return None
elif previous_has_break and multi_line_mode is False:
self.add_violation(ParametersIndentationViolation(node))
return None
return previous_has_break
def _check_indentation(
self,
node: ast.AST,
elements: Sequence[ast.AST],
extra_lines: int = 0, # we need it due to wrong lineno in collections
) -> None:
multi_line_mode: Optional[bool] = None
for index, statement in enumerate(elements):
if index == 0:
# We treat first element differently,
# since it is impossible to say what kind of multi-line
# parameters styles will be used at this moment.
multi_line_mode = self._check_first_element(
node,
statement,
extra_lines,
)
else:
multi_line_mode = self._check_rest_elements(
node,
statement,
elements[index - 1].lineno,
multi_line_mode,
)
@final
class PointlessStarredVisitor(BaseNodeVisitor):
"""Responsible for absence of useless starred expressions."""
_pointless_star_nodes: ClassVar[AnyNodes] = (
_pointless_star_nodes: ClassVar[types.AnyNodes] = (
ast.Dict,

@@ -384,3 +229,3 @@ ast.List,

ast.Tuple,
*TextNodes,
TextNodes,
)

@@ -399,5 +244,6 @@

for node in args:
if isinstance(node, ast.Starred):
if self._is_pointless_star(node.value):
self.add_violation(PointlessStarredViolation(node))
if isinstance(node, ast.Starred) and self._is_pointless_star(
node.value,
):
self.add_violation(PointlessStarredViolation(node))

@@ -425,3 +271,6 @@ def _check_double_starred_dict(

for key_node in node.value.keys:
if not isinstance(key_node, ast.Str):
if not (
isinstance(key_node, ast.Constant)
and isinstance(key_node.value, str)
):
return True

@@ -456,5 +305,8 @@ return False

for key_node in node.value.keys:
if isinstance(key_node, ast.Str):
if not str.isidentifier(key_node.s):
return True
if (
isinstance(key_node, ast.Constant)
and isinstance(key_node.value, str)
and not str.isidentifier(key_node.value)
):
return True
return False

@@ -480,5 +332,5 @@

is_checkable = (
len(node.targets) == 1 and
isinstance(node.value.right, ast.Name) and
isinstance(node.value.left, ast.Name)
len(node.targets) == 1
and isinstance(node.value.right, ast.Name)
and isinstance(node.value.left, ast.Name)
)

@@ -497,3 +349,3 @@

_no_tuples_collections: ClassVar[AnyNodes] = (
_no_tuples_collections: ClassVar[types.AnyNodes] = (
ast.List,

@@ -515,4 +367,4 @@ ast.ListComp,

is_checkable = (
isinstance(node.func, ast.Name) and
node.func.id in constants.TUPLE_ARGUMENTS_METHODS
isinstance(node.func, ast.Name)
and node.func.id in constants.TUPLE_ARGUMENTS_METHODS
)

@@ -519,0 +371,0 @@

import ast
from typing import Set
from typing import ClassVar, final
from typing_extensions import final
from wemake_python_styleguide.logic import source

@@ -20,3 +18,3 @@ from wemake_python_styleguide.logic.tree import functions, operators, slices

_marked_slices: Set[ast.Subscript] = set()
_marked_slices: ClassVar[set[ast.Subscript]] = set()

@@ -63,19 +61,14 @@ def visit_Subscript(self, node: ast.Subscript) -> None:

lower_ok = (
node.slice.lower is None or (
not self._is_zero(node.slice.lower) and
not self._is_none(node.slice.lower)
)
lower_ok = node.slice.lower is None or (
not self._is_zero(node.slice.lower)
and not self._is_none(node.slice.lower)
)
upper_ok = (
node.slice.upper is None or
not self._is_none(node.slice.upper)
upper_ok = node.slice.upper is None or not self._is_none(
node.slice.upper,
)
step_ok = (
node.slice.step is None or (
not self._is_one(node.slice.step) and
not self._is_none(node.slice.step)
)
step_ok = node.slice.step is None or (
not self._is_one(node.slice.step)
and not self._is_none(node.slice.step)
)

@@ -97,6 +90,6 @@

slice_expr = node.slice
slice_function_assignment = (
isinstance(slice_expr, ast.Call) and
functions.given_function_called(slice_expr, {'slice'})
)
slice_function_assignment = isinstance(
slice_expr,
ast.Call,
) and functions.given_function_called(slice_expr, {'slice'})

@@ -110,11 +103,17 @@ if subscript_slice_assignment or slice_function_assignment:

return (
isinstance(component_value, ast.NameConstant) and
component_value.value is None
isinstance(component_value, ast.Constant)
and component_value.value is None
)
def _is_zero(self, component_value: ast.expr) -> bool:
return isinstance(component_value, ast.Num) and component_value.n == 0
return (
isinstance(component_value, ast.Constant)
and component_value.value == 0
)
def _is_one(self, component_value: ast.expr) -> bool:
return isinstance(component_value, ast.Num) and component_value.n == 1
return (
isinstance(component_value, ast.Constant)
and component_value.value == 1
)

@@ -165,5 +164,5 @@

is_len_call = (
isinstance(node_slice, ast.BinOp) and
isinstance(node_slice.op, ast.Sub) and
self._is_wrong_len(
isinstance(node_slice, ast.BinOp)
and isinstance(node_slice.op, ast.Sub)
and self._is_wrong_len(
node_slice,

@@ -181,5 +180,5 @@ source.node_to_string(node.value),

return (
isinstance(node.left, ast.Call) and
bool(functions.given_function_called(node.left, {'len'})) and
source.node_to_string(node.left.args[0]) == element
isinstance(node.left, ast.Call)
and bool(functions.given_function_called(node.left, {'len'}))
and source.node_to_string(node.left.args[0]) == element
)

@@ -189,5 +188,4 @@

real_node = operators.unwrap_unary_node(node)
return (
isinstance(real_node, ast.Num) and
isinstance(real_node.n, float)
return isinstance(real_node, ast.Constant) and isinstance(
real_node.value, float
)

@@ -66,14 +66,13 @@ """

import tokenize
from typing import List, Sequence, Type
from collections.abc import Sequence
from typing import final
from typing_extensions import final
from wemake_python_styleguide import constants
from wemake_python_styleguide.compat.routing import route_visit
from wemake_python_styleguide.logic.filenames import get_stem
from wemake_python_styleguide.types import ConfigurationOptions
from wemake_python_styleguide.options.validation import ValidatedOptions
from wemake_python_styleguide.violations.base import BaseViolation
class BaseVisitor(metaclass=abc.ABCMeta):
class BaseVisitor(abc.ABC):
"""

@@ -92,3 +91,3 @@ Abstract base class for different types of visitors.

self,
options: ConfigurationOptions,
options: ValidatedOptions,
filename: str = constants.STDIN,

@@ -99,7 +98,7 @@ ) -> None:

self.filename = filename
self.violations: List[BaseViolation] = []
self.violations: list[BaseViolation] = []
@classmethod
def from_checker(
cls: Type['BaseVisitor'],
cls: type['BaseVisitor'],
checker,

@@ -122,2 +121,4 @@ ) -> 'BaseVisitor':

"""Adds violation to the visitor."""
# It is not allowed to add disabled violations:
assert violation.disabled_since is None, violation.code # noqa: S101
self.violations.append(violation)

@@ -157,3 +158,3 @@

self,
options: ConfigurationOptions,
options: ValidatedOptions,
tree: ast.AST,

@@ -169,3 +170,3 @@ **kwargs,

def from_checker(
cls: Type['BaseNodeVisitor'],
cls: type['BaseNodeVisitor'],
checker,

@@ -201,3 +202,3 @@ ) -> 'BaseNodeVisitor':

class BaseFilenameVisitor(BaseVisitor, metaclass=abc.ABCMeta):
class BaseFilenameVisitor(BaseVisitor, abc.ABC):
"""

@@ -245,3 +246,3 @@ Abstract base class that allows to visit and check module file names.

self,
options: ConfigurationOptions,
options: ValidatedOptions,
file_tokens: Sequence[tokenize.TokenInfo],

@@ -258,3 +259,3 @@ **kwargs,

def from_checker(
cls: Type['BaseTokenVisitor'],
cls: type['BaseTokenVisitor'],
checker,

@@ -287,3 +288,3 @@ ) -> 'BaseTokenVisitor':

token_type = tokenize.tok_name[token.exact_type].lower()
method = getattr(self, 'visit_{0}'.format(token_type), None)
method = getattr(self, f'visit_{token_type}', None)
if method is not None:

@@ -290,0 +291,0 @@ method(token)

@@ -1,2 +0,3 @@

from typing import Callable, Tuple, Type, TypeVar
from collections.abc import Callable
from typing import TypeVar

@@ -7,10 +8,10 @@ _DefinedType = TypeVar('_DefinedType')

def _modify_class(
cls: Type[_DefinedType],
cls: type[_DefinedType],
original: str,
aliases: Tuple[str, ...],
) -> Type[_DefinedType]:
aliases: tuple[str, ...],
) -> type[_DefinedType]:
original_handler = getattr(cls, original, None)
if original_handler is None:
raise AttributeError(
'Aliased attribute {0} does not exist'.format(original),
f'Aliased attribute {original} does not exist',
)

@@ -21,3 +22,3 @@

raise AttributeError(
'Alias {0} already exists'.format(method_alias),
f'Alias {method_alias} already exists',
)

@@ -30,4 +31,4 @@ setattr(cls, method_alias, original_handler)

original: str,
aliases: Tuple[str, ...],
) -> Callable[[Type[_DefinedType]], Type[_DefinedType]]:
aliases: tuple[str, ...],
) -> Callable[[type[_DefinedType]], type[_DefinedType]]:
"""

@@ -43,8 +44,9 @@ Decorator to alias handlers.

"""
all_names = aliases + (original, )
all_names = (*aliases, original)
if len(all_names) != len(set(all_names)):
raise ValueError('Found duplicate aliases')
def decorator(cls: Type[_DefinedType]) -> Type[_DefinedType]:
def decorator(cls: type[_DefinedType]) -> type[_DefinedType]:
return _modify_class(cls, original, aliases)
return decorator

@@ -1,2 +0,2 @@

from typing_extensions import final
from typing import final

@@ -28,5 +28,7 @@ from wemake_python_styleguide import constants

if access.is_magic(self.stem):
if self.stem not in constants.MAGIC_MODULE_NAMES_WHITELIST:
self.add_violation(naming.WrongModuleMagicNameViolation())
if (
access.is_magic(self.stem)
and self.stem not in constants.MAGIC_MODULE_NAMES_WHITELIST
):
self.add_violation(naming.WrongModuleMagicNameViolation())

@@ -73,3 +75,4 @@ if access.is_private(self.stem):

unreadable_sequence = alphabet.get_unreadable_characters(
self.stem, constants.UNREADABLE_CHARACTER_COMBINATIONS,
self.stem,
constants.UNREADABLE_CHARACTER_COMBINATIONS,
)

@@ -76,0 +79,0 @@ if unreadable_sequence:

@@ -22,6 +22,4 @@ r"""

from token import ENDMARKER
from typing import ClassVar, Pattern
from typing import ClassVar, Final, final
from typing_extensions import Final, final
from wemake_python_styleguide.constants import MAX_NO_COVER_COMMENTS, STDIN

@@ -57,4 +55,4 @@ from wemake_python_styleguide.logic.system import is_executable_file, is_windows

_no_cover: ClassVar[Pattern[str]] = re.compile(r'^pragma:\s+no\s+cover')
_type_check: ClassVar[Pattern[str]] = re.compile(
_no_cover: ClassVar[re.Pattern[str]] = re.compile(r'^pragma:\s+no\s+cover')
_type_check: ClassVar[re.Pattern[str]] = re.compile(
r'^type:\s?([\w\d\[\]\'\"\.]+)$',

@@ -146,5 +144,5 @@ )

# Empty comment right after non-empty, block not yet alerted
self._is_consecutive(self._prev_non_empty) and
self._in_same_block and
not self._block_alerted
self._is_consecutive(self._prev_non_empty)
and self._in_same_block
and not self._block_alerted
)

@@ -162,4 +160,4 @@ if to_reserve:

self._in_same_block = (
self._is_consecutive(self._prev_comment_line_num) and
token.line.lstrip()[0] == '#' # is inline comment
self._is_consecutive(self._prev_comment_line_num)
and token.line.lstrip()[0] == '#' # is inline comment
)

@@ -170,6 +168,6 @@ if not self._in_same_block:

def _is_consecutive(self, prev_line_num: int) -> bool:
return (self._line_num - prev_line_num == 1)
return self._line_num - prev_line_num == 1
def _has_reserved_token(self) -> bool:
return (self._reserved_token != SENTINEL_TOKEN)
return self._reserved_token != SENTINEL_TOKEN

@@ -189,3 +187,3 @@ def _post_visit(self) -> None:

_shebang: ClassVar[Pattern[str]] = re.compile(r'(\s*)#!')
_shebang: ClassVar[re.Pattern[str]] = re.compile(r'(\s*)#!')
_python_executable: ClassVar[str] = 'python'

@@ -257,3 +255,3 @@

return True
elif current_token.exact_type not in NEWLINES:
if current_token.exact_type not in NEWLINES:
break

@@ -271,3 +269,3 @@ current_token = next(all_tokens)

_noqa_check: ClassVar[Pattern[str]] = re.compile(
_noqa_check: ClassVar[re.Pattern[str]] = re.compile(
r'^(noqa:?)($|[A-Z\d\,\s]+)',

@@ -297,3 +295,3 @@ )

# We cannot pass the actual line here,
# since it will be ignored due to `# noqa` comment:
# since it will be ignored due to `noqa` comment:
self.add_violation(WrongMagicCommentViolation(text=comment_text))

@@ -314,3 +312,3 @@ return

for excluded in excludes_list:
if re.fullmatch(r'{0}($|\d+)'.format(noqa_code), excluded):
if re.fullmatch(rf'{noqa_code}($|\d+)', excluded):
self.add_violation(

@@ -317,0 +315,0 @@ ForbiddenInlineIgnoreViolation(text=str(noqa_excludes)),

import tokenize
from typing import ClassVar, FrozenSet, Sequence
from collections.abc import Sequence
from typing import ClassVar, final
from typing_extensions import final
from wemake_python_styleguide.violations.refactoring import (

@@ -41,13 +40,17 @@ ImplicitElifViolation,

_idents: ClassVar[FrozenSet[int]] = frozenset((
tokenize.INDENT,
tokenize.DEDENT,
))
_idents: ClassVar[frozenset[int]] = frozenset(
(
tokenize.INDENT,
tokenize.DEDENT,
),
)
_allowed_token_types: ClassVar[FrozenSet[int]] = frozenset((
tokenize.NEWLINE,
tokenize.NL,
tokenize.COLON,
tokenize.INDENT,
))
_allowed_token_types: ClassVar[frozenset[int]] = frozenset(
(
tokenize.NEWLINE,
tokenize.NL,
tokenize.COLON,
tokenize.INDENT,
),
)

@@ -65,7 +68,7 @@ def visit_name(self, token: tokenize.TokenInfo) -> None:

# There's a bug in coverage, I am not sure how to make it work.
next_tokens = self.file_tokens[token_index + 1:]
next_tokens = self.file_tokens[token_index + 1 :]
for index, next_token in enumerate(next_tokens): # pragma: no cover
if next_token.exact_type in self._allowed_token_types:
continue
elif next_token.string == 'if':
if next_token.string == 'if':
self._check_complex_else(next_tokens, next_token, index)

@@ -82,3 +85,3 @@ return

for token in reversed(self.file_tokens[:start_index - 1]):
for token in reversed(self.file_tokens[: start_index - 1]):
if token.type != tokenize.NAME:

@@ -128,3 +131,3 @@ continue

) -> None:
complex_else = self._if_has_code_below(tokens[index + 1:])
complex_else = self._if_has_code_below(tokens[index + 1 :])
if not complex_else:

@@ -131,0 +134,0 @@ self.add_violation(ImplicitElifViolation(current_token))

import math
import tokenize
from typing import Iterable, List, Optional, Tuple
from collections.abc import Iterable
from typing import final
from typing_extensions import final
from wemake_python_styleguide.violations import best_practices

@@ -13,4 +12,3 @@ from wemake_python_styleguide.visitors import base

class _Function:
def __init__(self, file_tokens: List[tokenize.TokenInfo]) -> None:
def __init__(self, file_tokens: list[tokenize.TokenInfo]) -> None:
self._tokens = file_tokens

@@ -40,7 +38,7 @@

class _FileFunctions:
def __init__(self, file_tokens: List[tokenize.TokenInfo]) -> None:
def __init__(self, file_tokens: list[tokenize.TokenInfo]) -> None:
self._file_tokens = file_tokens
def search_functions(self) -> Iterable[_Function]: # noqa: WPS210
function_tokens: List[tokenize.TokenInfo] = []
function_tokens: list[tokenize.TokenInfo] = []
in_function = False

@@ -73,3 +71,3 @@ function_start_token = (0, 0)

token_index: int,
function_start: Tuple[int, int],
function_start: tuple[int, int],
*,

@@ -80,6 +78,6 @@ function_tokens_exists: bool,

is_elipsis_end = (
next_token and
next_token.exact_type == tokenize.NEWLINE and
token.string == '...' and
token.start[0] == function_start[0]
next_token
and next_token.exact_type == tokenize.NEWLINE
and token.string == '...'
and token.start[0] == function_start[0]
)

@@ -95,3 +93,3 @@ if is_elipsis_end:

token_index: int,
) -> Optional[tokenize.TokenInfo]:
) -> tokenize.TokenInfo | None:
try:

@@ -105,3 +103,2 @@ return self._file_tokens[token_index + 1]

class _FileTokens:
def __init__(

@@ -118,7 +115,5 @@ self,

splitted_function_body = function.body().strip().split('\n')
empty_lines_count = len([
line
for line in splitted_function_body
if line == ''
])
empty_lines_count = len(
[line for line in splitted_function_body if not line],
)
if not empty_lines_count:

@@ -128,3 +123,4 @@ continue

available_empty_lines = self._available_empty_lines(
len(splitted_function_body), empty_lines_count,
len(splitted_function_body),
empty_lines_count,
)

@@ -157,3 +153,3 @@ if empty_lines_count > available_empty_lines:

super().__init__(*args, **kwargs)
self._file_tokens: List[tokenize.TokenInfo] = []
self._file_tokens: list[tokenize.TokenInfo] = []

@@ -160,0 +156,0 @@ def visit(self, token: tokenize.TokenInfo) -> None:

import re
import tokenize
from typing import Callable, ClassVar, FrozenSet, Optional, Pattern, Sequence
from collections.abc import Callable, Sequence
from typing import ClassVar, final
from typing_extensions import final
from wemake_python_styleguide.logic.tokens.numbers import (
has_correct_underscores,
)
from wemake_python_styleguide.logic.tokens.strings import (
get_docstring_tokens,
has_triple_string_quotes,
split_prefixes,

@@ -30,24 +30,11 @@ )

_bad_number_suffixes: ClassVar[Pattern[str]] = re.compile(
r'^[0-9\.]+[BOXE]',
_leading_zero_pattern: ClassVar[re.Pattern[str]] = re.compile(
r'^[0-9\.]+([box]|e\+?\-?)0.+',
re.IGNORECASE | re.ASCII,
)
_leading_zero_pattern: ClassVar[Pattern[str]] = re.compile(
r'^[0-9\.]+([box]|e\+?\-?)0.+', re.IGNORECASE | re.ASCII,
)
_leading_zero_float_pattern: ClassVar[Pattern[str]] = re.compile(
_leading_zero_float_pattern: ClassVar[re.Pattern[str]] = re.compile(
r'^[0-9]*\.[0-9]+0+$',
)
_positive_exponent_patterns: ClassVar[Pattern[str]] = re.compile(
r'^[0-9\.]+e\+', re.IGNORECASE | re.ASCII,
)
_bad_hex_numbers: ClassVar[FrozenSet[str]] = frozenset((
'a', 'b', 'c', 'd', 'e', 'f',
))
_bad_complex_suffix: ClassVar[str] = 'J'
_float_zero: ClassVar[Pattern[str]] = re.compile(
_float_zero: ClassVar[re.Pattern[str]] = re.compile(
r'^0\.0$',

@@ -63,19 +50,8 @@ )

"""
self._check_complex_suffix(token)
self._check_underscored_number(token)
self._check_partial_float(token)
self._check_bad_number_suffixes(token)
self._check_float_zeros(token)
def _check_complex_suffix(self, token: tokenize.TokenInfo) -> None:
if self._bad_complex_suffix in token.string:
self.add_violation(
consistency.BadComplexNumberSuffixViolation(
token,
text=self._bad_complex_suffix,
),
)
def _check_underscored_number(self, token: tokenize.TokenInfo) -> None:
if '_' in token.string:
if '_' in token.string and not has_correct_underscores(token.string):
self.add_violation(

@@ -88,14 +64,3 @@ consistency.UnderscoredNumberViolation(

def _check_partial_float(self, token: tokenize.TokenInfo) -> None:
if token.string.startswith('.') or token.string.endswith('.'):
self.add_violation(
consistency.PartialFloatViolation(token, text=token.string),
)
def _check_bad_number_suffixes(self, token: tokenize.TokenInfo) -> None:
if self._bad_number_suffixes.match(token.string):
self.add_violation(
consistency.BadNumberSuffixViolation(token, text=token.string),
)
float_zeros = self._leading_zero_float_pattern.match(token.string)

@@ -111,23 +76,2 @@ other_zeros = self._leading_zero_pattern.match(token.string)

if self._positive_exponent_patterns.match(token.string):
self.add_violation(
consistency.PositiveExponentViolation(
token,
text=token.string,
),
)
if token.string.startswith('0x') or token.string.startswith('0X'):
has_wrong_hex_numbers = any(
char in self._bad_hex_numbers
for char in token.string
)
if has_wrong_hex_numbers:
self.add_violation(
consistency.WrongHexNumberCaseViolation(
token,
text=token.string,
),
)
def _check_float_zeros(self, token: tokenize.TokenInfo) -> None:

@@ -142,11 +86,20 @@ if self._float_zero.match(token.string):

class _StringTokenChecker:
_bad_string_modifiers: ClassVar[FrozenSet[str]] = frozenset((
'R', 'F', 'B', 'U',
))
_bad_string_modifiers: ClassVar[frozenset[str]] = frozenset(
(
'R',
'F',
'B',
'U',
),
)
_unicode_escapes: ClassVar[FrozenSet[str]] = frozenset((
'u', 'U', 'N',
))
_unicode_escapes: ClassVar[frozenset[str]] = frozenset(
(
'u',
'U',
'N',
),
)
_implicit_raw_strings: ClassVar[Pattern[str]] = re.compile(r'\\{2}.+')
_implicit_raw_strings: ClassVar[re.Pattern[str]] = re.compile(r'\\{2}.+')

@@ -158,16 +111,4 @@ def __init__(

) -> None:
self._docstrings = get_docstring_tokens(file_tokens)
self._add_violation = add_violation
def check_correct_multiline(
self,
token: tokenize.TokenInfo,
string_def: str,
) -> None:
if has_triple_string_quotes(string_def):
if '\n' not in string_def and token not in self._docstrings:
self._add_violation(
consistency.WrongMultilineStringViolation(token),
)
def check_string_modifiers(

@@ -178,7 +119,2 @@ self,

) -> None:
if 'u' in modifiers.lower():
self._add_violation(
consistency.UnicodeStringViolation(token, text=token.string),
)
for modifier in modifiers:

@@ -234,14 +170,3 @@ if modifier in self._bad_string_modifiers:

def check_unnecessary_raw_string(
self,
token: tokenize.TokenInfo,
modifiers: str,
string_def: str,
) -> None:
if 'r' in modifiers.lower() and '\\' not in string_def:
self._add_violation(
consistency.RawStringNotNeededViolation(token, text=string_def),
)
@final

@@ -252,3 +177,3 @@ class WrongStringTokenVisitor(BaseTokenVisitor):

def __init__(self, *args, **kwargs) -> None:
"""Check string defitions."""
"""Check string definitions."""
super().__init__(*args, **kwargs)

@@ -269,9 +194,7 @@ self._checker = _StringTokenChecker(

modifiers, string_def = split_prefixes(token.string)
self._checker.check_correct_multiline(token, string_def)
self._checker.check_string_modifiers(token, modifiers)
self._checker.check_implicit_raw_string(token, modifiers, string_def)
self._checker.check_wrong_unicode_escape(token, modifiers, string_def)
self._checker.check_unnecessary_raw_string(token, modifiers, string_def)
def visit_fstring_start( # pragma: py-lt-312
def visit_fstring_start( # pragma: >3.12 cover
self,

@@ -316,35 +239,1 @@ token: tokenize.TokenInfo,

self._checker.check_string_modifiers(token, modifiers)
@final
class WrongStringConcatenationVisitor(BaseTokenVisitor):
"""Checks incorrect string concatenation."""
_ignored_tokens: ClassVar[FrozenSet[int]] = frozenset((
tokenize.NL,
tokenize.NEWLINE,
tokenize.INDENT,
tokenize.COMMENT,
))
def __init__(self, *args, **kwargs) -> None:
"""Adds extra ``_previous_token`` property."""
super().__init__(*args, **kwargs)
self._previous_token: Optional[tokenize.TokenInfo] = None
def visit(self, token: tokenize.TokenInfo) -> None:
"""Ensures that all string are concatenated as we allow."""
self._check_concatenation(token)
def _check_concatenation(self, token: tokenize.TokenInfo) -> None:
if token.exact_type in self._ignored_tokens:
return
if token.exact_type == tokenize.STRING:
if self._previous_token:
self.add_violation(
consistency.ImplicitStringConcatenationViolation(token),
)
self._previous_token = token
else:
self._previous_token = None
import tokenize
from collections import defaultdict
from operator import attrgetter
from typing import (
ClassVar,
DefaultDict,
Dict,
List,
Mapping,
Optional,
Sequence,
Tuple,
)
from typing import TypeAlias, final
from typing_extensions import TypeAlias, final
from wemake_python_styleguide.logic.tokens.brackets import (
get_reverse_bracket,
last_bracket,
)
from wemake_python_styleguide.logic.tokens.comprehensions import Compehension
from wemake_python_styleguide.logic.tokens.constants import (
ALLOWED_EMPTY_LINE_TOKENS,
)
from wemake_python_styleguide.logic.tokens.constants import (
MATCHING_BRACKETS as MATCHING,
)
from wemake_python_styleguide.logic.tokens.constants import NEWLINES
from wemake_python_styleguide.logic.tokens.newlines import next_meaningful_token
from wemake_python_styleguide.logic.tokens.queries import only_contains
from wemake_python_styleguide.logic.tokens.strings import (
get_docstring_tokens,
has_triple_string_quotes,
)
from wemake_python_styleguide.violations.best_practices import (
WrongMultilineStringUseViolation,
)
from wemake_python_styleguide.violations.consistency import (
BracketBlankLineViolation,
ExtraIndentationViolation,
InconsistentComprehensionViolation,
WrongBracketPositionViolation,
)
from wemake_python_styleguide.logic.tokens import strings
from wemake_python_styleguide.violations import best_practices, consistency
from wemake_python_styleguide.visitors.base import BaseTokenVisitor
from wemake_python_styleguide.visitors.decorators import alias
TokenLines: TypeAlias = DefaultDict[int, List[tokenize.TokenInfo]]
TokenLines: TypeAlias = defaultdict[int, list[tokenize.TokenInfo]]
@final
class ExtraIndentationVisitor(BaseTokenVisitor):
"""
Is used to find extra indentation in nodes.
Algorithm:
1. goes through all nodes in a module
2. remembers minimal indentation for each line
3. compares each two closest lines: indentation should not be >4
"""
_ignored_tokens: ClassVar[Tuple[int, ...]] = (
tokenize.NEWLINE,
)
_ignored_previous_token: ClassVar[Tuple[int, ...]] = (
tokenize.NL,
)
def __init__(self, *args, **kwargs) -> None:
"""Creates empty counter."""
super().__init__(*args, **kwargs)
self._offsets: Dict[int, tokenize.TokenInfo] = {}
def visit(self, token: tokenize.TokenInfo) -> None:
"""Goes through all tokens to find wrong indentation."""
self._check_extra_indentation(token)
def _check_extra_indentation(self, token: tokenize.TokenInfo) -> None:
lineno, _offset = token.start
if lineno not in self._offsets:
self._offsets[lineno] = token
def _get_token_offset(self, token: tokenize.TokenInfo) -> int:
if token.exact_type == tokenize.INDENT:
return token.end[1]
return token.start[1]
def _check_individual_line(
self,
lines: Sequence[int],
line: int,
index: int,
) -> None:
current_token = self._offsets[line]
if current_token.exact_type in self._ignored_tokens:
return
previous_token = self._offsets[lines[index - 1]]
if previous_token.exact_type in self._ignored_previous_token:
return
offset = self._get_token_offset(current_token)
previous_offset = self._get_token_offset(previous_token)
if offset > previous_offset + 4:
self.add_violation(ExtraIndentationViolation(current_token))
def _post_visit(self) -> None:
lines = sorted(self._offsets.keys())
for index, line in enumerate(lines):
if index == 0 or line != lines[index - 1] + 1:
continue
self._check_individual_line(lines, line, index)
@final
class BracketLocationVisitor(BaseTokenVisitor):
"""
Finds closing brackets location.
We check that brackets can be on the same line or
brackets can be the only tokens on the line.
We track all kind of brackets: round, square, and curly.
"""
def __init__(self, *args, **kwargs) -> None:
"""Creates line tracking for tokens."""
super().__init__(*args, **kwargs)
self._lines: TokenLines = defaultdict(list)
def visit(self, token: tokenize.TokenInfo) -> None:
"""Goes through all tokens to separate them by line numbers."""
self._lines[token.start[0]].append(token)
def _annotate_brackets(
self,
tokens: List[tokenize.TokenInfo],
) -> Mapping[int, int]:
"""Annotates each opening bracket with the nested level index."""
brackets = {bracket: 0 for bracket in MATCHING}
for token in tokens:
if token.exact_type in MATCHING.keys():
brackets[token.exact_type] += 1
if token.exact_type in MATCHING.values():
reverse_bracket = get_reverse_bracket(token)
if brackets[reverse_bracket] > 0:
brackets[reverse_bracket] -= 1
return brackets
def _check_closing(
self,
token: tokenize.TokenInfo,
index: int,
tokens: List[tokenize.TokenInfo],
) -> None:
tokens_before = tokens[:index]
annotated = self._annotate_brackets(tokens_before)
if annotated[get_reverse_bracket(token)] == 0:
if not only_contains(tokens_before, ALLOWED_EMPTY_LINE_TOKENS):
self.add_violation(WrongBracketPositionViolation(token))
def _check_individual_line(self, tokens: List[tokenize.TokenInfo]) -> None:
for index, token in enumerate(tokens):
if token.exact_type in MATCHING.values():
self._check_closing(token, index, tokens)
if index == 0:
self._check_empty_line_wrap(token, delta=-1)
elif token.exact_type in MATCHING and last_bracket(tokens, index):
self._check_empty_line_wrap(token, delta=1)
def _check_empty_line_wrap(
self,
token: tokenize.TokenInfo,
*,
delta: int,
) -> None:
tokens = self._lines.get(token.start[0] + delta)
if tokens is not None and only_contains(tokens, NEWLINES):
self.add_violation(BracketBlankLineViolation(token))
def _post_visit(self) -> None:
for tokens in self._lines.values():
self._check_individual_line(tokens)
@final
class MultilineStringVisitor(BaseTokenVisitor):

@@ -197,3 +21,2 @@ """Checks if multiline strings are used only in assignment or docstrings."""

self._lines: TokenLines = defaultdict(list)
self._docstrings = get_docstring_tokens(self.file_tokens)

@@ -204,36 +27,79 @@ def visit(self, token: tokenize.TokenInfo) -> None:

def _check_token(
def _check_multiline_usage(
self,
index: int,
tokens: List[tokenize.TokenInfo],
previous_token: Optional[tokenize.TokenInfo],
next_token: Optional[tokenize.TokenInfo],
tokens: list[tokenize.TokenInfo],
meaningful_tokens: list[tokenize.TokenInfo],
previous_token: tokenize.TokenInfo | None,
next_token: tokenize.TokenInfo | None,
) -> None:
if index != 0:
previous_token = tokens[index - 1]
if index + 1 < len(tokens):
next_token = tokens[index + 1]
if len(meaningful_tokens) == 1:
# We allow simple string tokens to be present anywhere, for example:
# ```python
# class Example:
# """Docstring.""" # <- this should be allowed
# x: int # noqa: ERA001
# """Attr docs.""" # <- this should be allowed
# ```
return
if previous_token and previous_token.exact_type != tokenize.EQUAL:
self.add_violation(WrongMultilineStringUseViolation(previous_token))
self.add_violation(
best_practices.WrongMultilineStringUseViolation(previous_token)
)
if index + 1 < len(tokens):
next_token = tokens[index + 1]
if next_token and next_token.exact_type == tokenize.DOT:
self.add_violation(WrongMultilineStringUseViolation(next_token))
self.add_violation(
best_practices.WrongMultilineStringUseViolation(next_token)
)
def _check_useless_multiline(
self,
token: tokenize.TokenInfo,
meaningful_tokens: list[tokenize.TokenInfo],
) -> None:
if len(meaningful_tokens) == 1:
return # We always allow just multiline strings on a single line.
_modifiers, string_def = strings.split_prefixes(token.string)
if '\n' in string_def:
return # Strings with newlines are fine
self.add_violation(
consistency.UselessMultilineStringViolation(token),
)
def _check_individual_line(
self,
tokens: List[tokenize.TokenInfo],
previous_token: Optional[tokenize.TokenInfo],
next_token: Optional[tokenize.TokenInfo],
tokens: list[tokenize.TokenInfo],
previous_token: tokenize.TokenInfo | None,
next_token: tokenize.TokenInfo | None,
) -> None:
for index, token in enumerate(tokens):
if token.exact_type != tokenize.STRING or token in self._docstrings:
if (
token.exact_type != tokenize.STRING
or not strings.has_triple_string_quotes(token.string)
):
continue
if has_triple_string_quotes(token.string):
self._check_token(index, tokens, previous_token, next_token)
meaningful_tokens = list(
filter(strings.is_meaningful_token, tokens),
)
self._check_useless_multiline(token, meaningful_tokens)
self._check_multiline_usage(
index,
tokens,
meaningful_tokens,
previous_token,
next_token,
)
def _post_visit(self) -> None:
linenos = sorted((self._lines.keys()))
linenos = sorted(self._lines.keys())
for index, _ in enumerate(linenos):
line_tokens = sorted(
self._lines[linenos[index]], key=attrgetter('start'),
self._lines[linenos[index]],
key=attrgetter('start'),
)

@@ -243,115 +109,15 @@ previous_line_token = None

if index != 0:
previous_line_token = sorted(
self._lines[linenos[index - 1]], key=attrgetter('start'),
)[-1]
previous_line_token = max(
self._lines[linenos[index - 1]],
key=attrgetter('start'),
)
if index + 1 < len(linenos):
next_line_token = sorted(
self._lines[linenos[index + 1]], key=attrgetter('start'),
)[0]
next_line_token = min(
self._lines[linenos[index + 1]],
key=attrgetter('start'),
)
self._check_individual_line(
line_tokens, previous_line_token, next_line_token,
line_tokens,
previous_line_token,
next_line_token,
)
@final
@alias('visit_any_left_bracket', (
'visit_lsqb',
'visit_lbrace',
'visit_lpar',
))
@alias('visit_any_right_bracket', (
'visit_rsqb',
'visit_rbrace',
'visit_rpar',
))
class InconsistentComprehensionVisitor(BaseTokenVisitor):
"""
Visitor for checking inconsistent comprehension syntax.
Checks if comprehensions either use only one line or inserts a newline
for each clause (i.e. bracket, action, for loop(s), and conditional)
"""
def __init__(self, *args, **kwargs) -> None:
"""
Initializes stack of bracket contexts.
Creates an empty stack for bracket contexts to accommodate for nested
comprehensions.
"""
super().__init__(*args, **kwargs)
self._bracket_stack: List[Compehension] = []
self._current_ctx: Optional[Compehension] = None
def visit_any_left_bracket(self, token: tokenize.TokenInfo) -> None:
"""Sets self._inside_brackets to True if left bracket found."""
self._current_ctx = Compehension(left_bracket=token)
self._bracket_stack.append(self._current_ctx)
def visit_any_right_bracket(self, token: tokenize.TokenInfo) -> None:
"""Resets environment if right bracket is encountered."""
previous_ctx = self._bracket_stack.pop()
if previous_ctx.is_ready() and not previous_ctx.is_valid():
self.add_violation(
InconsistentComprehensionViolation(previous_ctx.fors[-1]),
)
self._current_ctx = (
self._bracket_stack[-1] if self._bracket_stack else None
)
def visit_name(self, token: tokenize.TokenInfo) -> None:
"""Builds the comprehension."""
if not self._current_ctx:
return
if token.string == 'async':
self._apply_async(token)
elif token.string == 'for':
self._apply_expr(token)
self._current_ctx.fors.append(token)
elif token.string == 'in':
self._apply_in_expr(token)
self._current_ctx.ins.append(token)
elif token.string == 'if':
self._current_ctx.append_if(token)
def _apply_async(self, token: tokenize.TokenInfo) -> None:
assert self._current_ctx # noqa: S101
# `for` is always next due to grammar rules,
# you can try to add a comment there, but we don't allow it
for_token = self.file_tokens[self.file_tokens.index(token) + 1]
is_broken = (
for_token.string != 'for' or
token.start[0] != for_token.start[0]
)
if is_broken:
self._current_ctx.async_broken = True
def _apply_expr(self, token: tokenize.TokenInfo) -> None:
assert self._current_ctx # noqa: S101
if self._current_ctx.expr:
return # we set this value only once
# What we do here:
# 1. We find an opening bracket
# 2. Then we find the next meaningful (non-NL) token
# that represents the actual expr of a comprehension
# 3. We assign it to the current comprehension structure
token_index = self.file_tokens.index(self._current_ctx.left_bracket)
self._current_ctx.expr = next_meaningful_token(
self.file_tokens,
token_index,
)
def _apply_in_expr(self, token: tokenize.TokenInfo) -> None:
assert self._current_ctx # noqa: S101
# This is not the whole expression, but we only need where it starts:
token_index = self.file_tokens.index(token)
self._current_ctx.in_exprs.append(next_meaningful_token(
self.file_tokens,
token_index,
))

@@ -1,10 +0,6 @@

import keyword
import tokenize
from typing import final
from typing_extensions import final
from wemake_python_styleguide.violations.consistency import (
LineCompriseCarriageReturnViolation,
LineStartsWithDotViolation,
MissingSpaceBetweenKeywordAndParenViolation,
)

@@ -16,17 +12,12 @@ from wemake_python_styleguide.visitors.base import BaseTokenVisitor

@final
@alias('visit_any_newline', (
'visit_newline',
'visit_nl',
))
@alias(
'visit_any_newline',
(
'visit_newline',
'visit_nl',
),
)
class WrongKeywordTokenVisitor(BaseTokenVisitor):
"""Visits keywords and finds violations related to their usage."""
def visit_name(self, token: tokenize.TokenInfo) -> None:
"""Check keywords related rules."""
self._check_space_before_open_paren(token)
def visit_dot(self, token: tokenize.TokenInfo) -> None:
"""Checks newline related rules."""
self._check_line_starts_with_dot(token)
def visit_any_newline(self, token: tokenize.TokenInfo) -> None:

@@ -36,20 +27,7 @@ r"""Checks ``\r`` (carriage return) in line breaks."""

def _check_space_before_open_paren(self, token: tokenize.TokenInfo) -> None:
if not keyword.iskeyword(token.string):
return
if token.line[token.end[1]:].startswith('('):
self.add_violation(
MissingSpaceBetweenKeywordAndParenViolation(token),
)
def _check_line_starts_with_dot(self, token: tokenize.TokenInfo) -> None:
line = token.line.lstrip()
if line.startswith('.') and not line.startswith('...'):
self.add_violation(LineStartsWithDotViolation(token))
def _check_line_comprise_carriage_return(
self, token: tokenize.TokenInfo,
self,
token: tokenize.TokenInfo,
) -> None:
if '\r' in token.string:
self.add_violation(LineCompriseCarriageReturnViolation(token))
import ast
from typing import Any, Optional, Union
def _convert_num(node: Optional[ast.AST]) -> Any:
if isinstance(node, ast.Constant):
if isinstance(node.value, (int, float, complex)):
return node.value
# That's what is modified from the original
elif isinstance(node, ast.Name):
# We return string names as is, see how we return strings:
return node.id
raise ValueError('malformed node or string: {0!r}'.format(node))
def _convert_signed_num(node: Optional[ast.AST]) -> Any:
unary_operators = (ast.UAdd, ast.USub)
if isinstance(node, ast.UnaryOp) and isinstance(node.op, unary_operators):
operand = _convert_num(node.operand)
return +operand if isinstance(node.op, ast.UAdd) else -operand
return _convert_num(node)
def _convert_complex(node: ast.BinOp) -> Any:
left = _convert_signed_num(node.left)
right = _convert_num(node.right)
if isinstance(left, (int, float)) and isinstance(right, complex):
if isinstance(node.op, ast.Add):
return left + right
return left - right
return None
def _convert_iterable(
node: Union[ast.Tuple, ast.List, ast.Set, ast.Dict],
) -> Any:
if isinstance(node, ast.Tuple):
return tuple(map(literal_eval_with_names, node.elts))
elif isinstance(node, ast.List):
return list(map(literal_eval_with_names, node.elts))
elif isinstance(node, ast.Set):
return set(map(literal_eval_with_names, node.elts))
return dict(zip(
map(literal_eval_with_names, node.keys),
map(literal_eval_with_names, node.values),
))
def literal_eval_with_names( # noqa: WPS231
node: Optional[ast.AST],
) -> Any:
"""
Safely evaluate constants and ``ast.Name`` nodes.
We need this function to tell
that ``[name]`` and ``[name]`` are the same nodes.
Copied from the CPython's source code.
Modified to treat ``ast.Name`` nodes as constants.
See: :py:`ast.literal_eval` source.
We intentionally ignore complexity violation here,
because we try to stay as close to the original source as possible.
"""
binary_operators = (ast.Add, ast.Sub)
if isinstance(node, (ast.Constant, ast.NameConstant)):
return node.value
elif isinstance(node, (ast.Tuple, ast.List, ast.Set, ast.Dict)):
return _convert_iterable(node)
elif isinstance(node, ast.BinOp) and isinstance(node.op, binary_operators):
maybe_complex = _convert_complex(node)
if maybe_complex is not None:
return maybe_complex
return _convert_signed_num(node)
import ast
from collections import defaultdict
from typing import ClassVar, DefaultDict, Set, cast
from typing_extensions import final
from wemake_python_styleguide.logic.naming import access, name_nodes
from wemake_python_styleguide.logic.nodes import get_context
from wemake_python_styleguide.types import ContextNodes
#: That's how we represent scopes that are bound to contexts.
_ContextStore = DefaultDict[ContextNodes, Set[str]]
class _BaseScope:
"""Base class for scope operations."""
@final
def __init__(self, node: ast.AST) -> None:
"""Saving current node and context."""
self._node = node
self._context = cast(ContextNodes, get_context(self._node))
def add_to_scope(self, names: Set[str]) -> None: # pragma: no cover
"""Adds a given set of names to some scope."""
raise NotImplementedError()
def shadowing(self, names: Set[str]) -> Set[str]: # pragma: no cover
"""Tells either some shadowing exist between existing scopes."""
raise NotImplementedError()
@final
def _exclude_unused(self, names: Set[str]) -> Set[str]:
"""Removes unused variables from set of names."""
return {
var_name # we allow to reuse explicit `_` variables
for var_name in names
if not access.is_unused(var_name)
}
@final
class BlockScope(_BaseScope):
"""Represents the visibility scope of a variable in a block."""
#: Updated when we have a new block variable.
_block_scopes: ClassVar[_ContextStore] = defaultdict(set)
#: Updated when we have a new local variable.
_local_scopes: ClassVar[_ContextStore] = defaultdict(set)
def add_to_scope(
self,
names: Set[str],
*,
is_local: bool = False,
) -> None:
"""Adds a set of names to the specified scope."""
scope = self._get_scope(is_local=is_local)
scope[self._context] = scope[self._context].union(
self._exclude_unused(names),
)
def shadowing(
self,
names: Set[str],
*,
is_local: bool = False,
) -> Set[str]:
"""Calculates the intersection for a set of names and a context."""
if not names:
return set()
scope = self._get_scope(is_local=not is_local)
current_names = scope[self._context]
if not is_local:
# Why do we care to update the scope for block variables?
# Because, block variables cannot shadow each other.
scope = self._get_scope(is_local=is_local)
current_names = current_names.union(scope[self._context])
return set(current_names).intersection(names)
def _get_scope(self, *, is_local: bool = False) -> _ContextStore:
return self._local_scopes if is_local else self._block_scopes
@final
class OuterScope(_BaseScope):
"""Represents scoping store to check name shadowing."""
_scopes: ClassVar[_ContextStore] = defaultdict(set)
def add_to_scope(self, names: Set[str]) -> None:
"""Adds a set of variables to the context scope."""
if isinstance(self._context, ast.ClassDef):
# Class names are not available to the caller directly.
return
self._scopes[self._context] = self._scopes[self._context].union(
self._exclude_unused(names),
)
def shadowing(self, names: Set[str]) -> Set[str]:
"""Calculates the intersection for a set of names and a context."""
if isinstance(self._context, ast.ClassDef):
# Class names are not available to the caller directly.
return set()
current_names = self._build_outer_context()
return set(current_names).intersection(names)
def _build_outer_context(self) -> Set[str]:
outer_names: Set[str] = set()
context = self._context
while True:
context = cast(ContextNodes, get_context(context))
outer_names = outer_names.union(self._scopes[context])
if not context: # type: ignore
break
return outer_names
def extract_names(node: ast.AST) -> Set[str]:
"""Extracts unique set of names from a given node."""
return set(name_nodes.get_variables_from_node(node))
import ast
from typing import Set
from typing_extensions import Final
from wemake_python_styleguide.compat.aliases import AssignNodes, FunctionNodes
from wemake_python_styleguide.logic.nodes import get_parent
from wemake_python_styleguide.logic.source import node_to_string
#: That's what we expect from `@overload` decorator:
_OVERLOAD_EXCEPTIONS: Final = frozenset(('overload', 'typing.overload'))
#: That's what we expect from `@property` decorator:
_PROPERTY_EXCEPTIONS: Final = frozenset(('property', '.setter'))
# Name predicates:
def is_function_overload(node: ast.AST) -> bool:
"""Check that function decorated with `typing.overload`."""
if isinstance(node, FunctionNodes):
for decorator in node.decorator_list:
if node_to_string(decorator) in _OVERLOAD_EXCEPTIONS:
return True
return False
def is_no_value_annotation(node: ast.AST) -> bool:
"""Check that variable has annotation without value."""
return isinstance(node, ast.AnnAssign) and not node.value
def is_property_setter(node: ast.AST) -> bool:
"""Check that function decorated with ``@property.setter``."""
if isinstance(node, FunctionNodes):
for decorator in node.decorator_list:
if node_to_string(decorator) in _PROPERTY_EXCEPTIONS:
return True
return False
# Scope predicates:
def is_same_value_reuse(node: ast.AST, names: Set[str]) -> bool:
"""Checks if the given names are reused by the given node."""
if isinstance(node, AssignNodes) and node.value:
used_names = {
name_node.id
for name_node in ast.walk(node.value)
if isinstance(name_node, ast.Name)
}
if not names.difference(used_names):
return True
return False
def is_same_try_except_cases(node: ast.AST, names: Set[str]) -> bool:
"""Same names in different ``except`` blocks are not counted."""
if not isinstance(node, ast.ExceptHandler):
return False
for except_handler in getattr(get_parent(node), 'handlers', []):
if except_handler.name and except_handler.name == node.name:
if except_handler is not node:
return True
return False
import tokenize
from typing import List
from wemake_python_styleguide.logic.tokens.constants import (
MATCHING_BRACKETS,
NEWLINES,
)
from wemake_python_styleguide.logic.tokens.queries import only_contains
def get_reverse_bracket(bracket: tokenize.TokenInfo) -> int:
"""
Returns the reverse closing bracket for an opening token.
>>> import tokenize
>>> import token
>>> bracket = tokenize.TokenInfo(token.RPAR, ")", 6, 7, "(a, b)")
>>> get_reverse_bracket(bracket) == token.LPAR
True
"""
index = list(MATCHING_BRACKETS.values()).index(bracket.exact_type)
return list(MATCHING_BRACKETS.keys())[index]
def last_bracket(tokens: List[tokenize.TokenInfo], index: int) -> bool:
"""Tells whether the given index is the last bracket token in line."""
return only_contains(
tokens[index + 1:],
NEWLINES.union({tokenize.COMMENT}),
)
import tokenize
from typing import List, Optional
import attr
from typing_extensions import final
@final
@attr.dataclass(slots=True)
class Compehension:
"""
Represents a syntax for Python comprehension.
The optimal way of using this class is
by just creating it with the first opening ``left_bracket``
and then assigning values you need when you meet them.
"""
left_bracket: tokenize.TokenInfo
expr: Optional[tokenize.TokenInfo] = None
# `for` keywords
fors: List[tokenize.TokenInfo] = attr.ib(factory=list)
# `in` part, keywords and expressions
ins: List[tokenize.TokenInfo] = attr.ib(factory=list)
in_exprs: List[tokenize.TokenInfo] = attr.ib(factory=list)
# Condition part:
_ifs: List[tokenize.TokenInfo] = attr.ib(factory=list)
async_broken: bool = False
_checked: bool = False
def append_if(self, token: tokenize.TokenInfo) -> None:
"""
Conditionally appends ``if`` token, if there's at least one ``for``.
Why? Because you might have ``if`` before ``for``.
In this case it is just a ternary inside ``expr``.
In real comprehensions ``if`` are always after ``for``.
"""
if self.fors:
self._ifs.append(token)
def is_ready(self) -> bool:
"""
Checks that comprehension is built correctly with all required parts.
We also check that each compehension is analyzed only once.
"""
return (
self.expr is not None and
bool(self.fors) and
len(self.fors) == len(self.ins) == len(self.in_exprs) and
not self._checked
)
def is_valid(self) -> bool:
"""Checks that compehension definition is valid."""
if self.async_broken:
return False
for_in = self._check_for_in()
# mypy requires this `assert`, always true if `is_ready()`
assert self.expr # noqa: S101
is_multiline = self.expr.start[0] != self._first_for_line
fors = self._check_fors(is_multiline=is_multiline)
for_if = self._check_for_if(is_multiline=is_multiline)
self._checked = True # noqa: WPS601
return for_in and fors and for_if
@property
def _first_for_line(self) -> int:
"""Returns the line number of the first ``for`` token."""
return self.fors[0].start[0]
def _check_for_in(self) -> bool:
"""Checks that all ``for`` and ``in`` tokens are aligned together."""
return all(
for_.start[0] == in_.start[0] == in_expr.start[0]
for for_, in_, in_expr in zip(self.fors, self.ins, self.in_exprs)
)
def _check_fors(self, *, is_multiline: bool) -> bool:
"""Checks that all ``for`` tokens are aligned."""
if len(self.fors) == 1:
return True # one `for` is always correct
if is_multiline:
return all(
for_.start[0] == self._first_for_line + index
for index, for_ in enumerate(self.fors)
if index > 0
)
return all(
for_.start[0] == self._first_for_line
for for_ in self.fors
)
def _check_for_if(self, *, is_multiline: bool) -> bool:
"""Checks that all ``for`` and ``if`` tokens are aligned."""
if is_multiline:
last_for_line = self.fors[-1].start[0]
return all(
if_.start[0] == last_for_line + index + 1
for index, if_ in enumerate(self._ifs)
)
return all(
if_.start[0] == self._first_for_line
for if_ in self._ifs
)
import tokenize
from typing import Container, Iterable
def only_contains(
tokens: Iterable[tokenize.TokenInfo],
container: Container[int],
) -> bool:
"""Determines that only tokens from the given list are contained."""
return all(
token.exact_type in container
for token in tokens
)
import ast
from wemake_python_styleguide.logic.nodes import get_parent
def fix_line_number(tree: ast.AST) -> ast.AST:
"""
Adjusts line number for some nodes.
They are set incorrectly for some collections.
It might be either a bug or a feature.
We do several checks here, to be sure that we won't get
an incorrect line number. But, we basically check if there's
a parent, so we can compare and adjust.
Example::
print(( # should start from here
1, 2, 3, # actually starts from here
))
"""
affected = (ast.Tuple,)
for node in ast.walk(tree):
if isinstance(node, affected):
parent_lineno = getattr(get_parent(node), 'lineno', None)
if parent_lineno and parent_lineno < node.lineno:
node.lineno = node.lineno - 1
return tree
import ast
from typing_extensions import final
from wemake_python_styleguide.types import AnyFunctionDef
from wemake_python_styleguide.violations.consistency import (
MultilineFunctionAnnotationViolation,
)
from wemake_python_styleguide.visitors.base import BaseNodeVisitor
from wemake_python_styleguide.visitors.decorators import alias
@final
@alias('visit_any_function', (
'visit_FunctionDef',
'visit_AsyncFunctionDef',
))
class WrongAnnotationVisitor(BaseNodeVisitor):
"""Ensures that annotations are used correctly."""
def visit_any_function(self, node: AnyFunctionDef) -> None:
"""Checks return type annotations."""
self._check_return_annotation(node)
self.generic_visit(node)
def visit_arg(self, node: ast.arg) -> None:
"""Checks arguments annotations."""
self._check_arg_annotation(node)
self.generic_visit(node)
def _check_return_annotation(self, node: AnyFunctionDef) -> None:
if not node.returns:
return
for sub_node in ast.walk(node.returns):
lineno = getattr(sub_node, 'lineno', None)
if lineno and lineno != node.returns.lineno:
self.add_violation(MultilineFunctionAnnotationViolation(node))
return
def _check_arg_annotation(self, node: ast.arg) -> None:
for sub_node in ast.walk(node):
lineno = getattr(sub_node, 'lineno', None)
if lineno and lineno != node.lineno:
self.add_violation(MultilineFunctionAnnotationViolation(node))
return
import ast
from typing import ClassVar, FrozenSet
from typing_extensions import final
from wemake_python_styleguide.compat.aliases import FunctionNodes
from wemake_python_styleguide.constants import ALL_MAGIC_METHODS
from wemake_python_styleguide.logic import nodes
from wemake_python_styleguide.logic.naming import access
from wemake_python_styleguide.violations.best_practices import (
ProtectedAttributeViolation,
)
from wemake_python_styleguide.violations.oop import (
DirectMagicAttributeAccessViolation,
)
from wemake_python_styleguide.visitors.base import BaseNodeVisitor
@final
class WrongAttributeVisitor(BaseNodeVisitor):
"""Ensures that attributes are used correctly."""
_allowed_to_use_protected: ClassVar[FrozenSet[str]] = frozenset((
'self',
'cls',
'mcs',
))
def visit_Attribute(self, node: ast.Attribute) -> None:
"""Checks the `Attribute` node."""
self._check_protected_attribute(node)
self._check_magic_attribute(node)
self.generic_visit(node)
def _is_super_called(self, node: ast.Call) -> bool:
return isinstance(node.func, ast.Name) and node.func.id == 'super'
def _ensure_attribute_type(self, node: ast.Attribute, exception) -> None:
if isinstance(node.value, ast.Name):
if node.value.id in self._allowed_to_use_protected:
return
if isinstance(node.value, ast.Call):
if self._is_super_called(node.value):
return
self.add_violation(exception(node, text=node.attr))
def _check_protected_attribute(self, node: ast.Attribute) -> None:
if access.is_protected(node.attr):
self._ensure_attribute_type(node, ProtectedAttributeViolation)
def _check_magic_attribute(self, node: ast.Attribute) -> None:
if access.is_magic(node.attr):
# If "magic" method being called has the same name as
# the enclosing function, then it is a "wrapper" and thus
# a "false positive".
ctx = nodes.get_context(node)
if isinstance(ctx, FunctionNodes):
if node.attr == ctx.name:
return
if node.attr in ALL_MAGIC_METHODS:
self._ensure_attribute_type(
node, DirectMagicAttributeAccessViolation,
)

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