wemake-python-styleguide
Advanced tools
| 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, |
+195
-40
@@ -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
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
658382
-7.48%146
-2.67%17301
-6.74%