wemake-python-styleguide
Advanced tools
| import ast | ||
| from typing import final | ||
| from wemake_python_styleguide.violations import complexity | ||
| from wemake_python_styleguide.visitors.base import BaseNodeVisitor | ||
| @final | ||
| class ComplexFinallyBlocksVisitor(BaseNodeVisitor): | ||
| """Ensures there are no complex ``finally`` blocks.""" | ||
| def visit_Try(self, node: ast.Try) -> None: | ||
| """Visits all finally nodes in the tree.""" | ||
| self._check_complex_finally(node) | ||
| self.generic_visit(node) | ||
| def _check_complex_finally(self, node: ast.Try) -> None: | ||
| """Checks complexity of finally blocks.""" | ||
| if not node.finalbody: | ||
| return | ||
| first_line = node.finalbody[0].lineno | ||
| # `end_lineno` was added in 3.8, but typing is not really correct, | ||
| # we are pretty sure that it always exist in modern python versions. | ||
| last_line = getattr(node.finalbody[-1], 'end_lineno', 0) or 0 | ||
| total_lines = last_line - first_line + 1 | ||
| if total_lines > self.options.max_lines_in_finally: | ||
| self.add_violation( | ||
| complexity.ComplexFinallyViolation( | ||
| node, | ||
| text=str(total_lines), | ||
| baseline=self.options.max_lines_in_finally, | ||
| ), | ||
| ) |
+3
-9
| Metadata-Version: 2.3 | ||
| Name: wemake-python-styleguide | ||
| Version: 1.1.0 | ||
| Version: 1.2.0 | ||
| Summary: The strictest and most opinionated python linter ever | ||
@@ -14,9 +14,3 @@ License: MIT | ||
| Classifier: Intended Audience :: Developers | ||
| Classifier: License :: OSI Approved :: MIT License | ||
| Classifier: Operating System :: OS Independent | ||
| Classifier: Programming Language :: Python :: 3 | ||
| Classifier: Programming Language :: Python :: 3.10 | ||
| Classifier: Programming Language :: Python :: 3.11 | ||
| Classifier: Programming Language :: Python :: 3.12 | ||
| Classifier: Programming Language :: Python :: 3.13 | ||
| Classifier: Topic :: Software Development :: Libraries :: Python Modules | ||
@@ -26,4 +20,4 @@ Classifier: Topic :: Software Development :: Quality Assurance | ||
| Requires-Dist: attrs | ||
| Requires-Dist: flake8 (>=7.1,<8.0) | ||
| Requires-Dist: pygments (>=2.5,<3.0) | ||
| Requires-Dist: flake8 (>=7.3,<8.0) | ||
| Requires-Dist: pygments (>=2.19,<3.0) | ||
| Project-URL: Funding, https://opencollective.com/wemake-python-styleguide | ||
@@ -30,0 +24,0 @@ Project-URL: Homepage, https://wemake-python-styleguide.rtfd.io |
+19
-19
@@ -5,11 +5,11 @@ [build-system] | ||
| [tool.poetry] | ||
| [project] | ||
| name = "wemake-python-styleguide" | ||
| version = "1.1.0" | ||
| version = "1.2.0" | ||
| description = "The strictest and most opinionated python linter ever" | ||
| license = "MIT" | ||
| license = {text = "MIT"} | ||
| authors = [ | ||
| "Nikita Sobolev <mail@sobolevn.me>", | ||
| {name = "Nikita Sobolev", email = "mail@sobolevn.me"} | ||
| ] | ||
@@ -19,5 +19,2 @@ | ||
| repository = "https://github.com/wemake-services/wemake-python-styleguide" | ||
| homepage = "https://wemake-python-styleguide.rtfd.io" | ||
| keywords = [ | ||
@@ -45,4 +42,6 @@ "flake8", | ||
| [tool.poetry.urls] | ||
| "Funding" = "https://opencollective.com/wemake-python-styleguide" | ||
| [project.urls] | ||
| Homepage = "https://wemake-python-styleguide.rtfd.io" | ||
| Repository = "https://github.com/wemake-services/wemake-python-styleguide" | ||
| Funding = "https://opencollective.com/wemake-python-styleguide" | ||
@@ -58,18 +57,18 @@ [tool.poetry.plugins."flake8.extension"] | ||
| flake8 = "^7.1" | ||
| flake8 = "^7.3" | ||
| attrs = "*" | ||
| pygments = "^2.5" | ||
| pygments = "^2.19" | ||
| [tool.poetry.group.dev.dependencies] | ||
| pytest = "^8.1" | ||
| pytest-cov = "^6.0" | ||
| pytest = "^8.4" | ||
| pytest-cov = "^6.2" | ||
| pytest-randomly = "^3.12" | ||
| pytest-xdist = "^3.6" | ||
| pytest-xdist = "^3.8" | ||
| covdefaults = "^2.3" | ||
| syrupy = "^4.6" | ||
| hypothesis = "^6.35" | ||
| hypothesis = "^6.135" | ||
| hypothesmith = "^0.3" | ||
| mypy = "^1.13" | ||
| types-flake8 = "^7.1" | ||
| mypy = "^1.16" | ||
| types-flake8 = "^7.3" | ||
@@ -79,5 +78,5 @@ import-linter = "^2.0" | ||
| astpath = "^0.9" | ||
| lxml = "^5.1" | ||
| lxml = "^6.0" | ||
| nbqa = "^1.2" | ||
| ruff = "^0.11" | ||
| ruff = "^0.12" | ||
| black = "^25.1" | ||
@@ -123,2 +122,3 @@ | ||
| quote-style = "single" | ||
| # This is only required because we have invalid on-purpose code in docstrings: | ||
| docstring-code-format = false | ||
@@ -125,0 +125,0 @@ |
@@ -11,2 +11,3 @@ """ | ||
| import ast | ||
| from collections.abc import Container | ||
| from typing import TypeAlias | ||
@@ -48,1 +49,28 @@ | ||
| return 1 | ||
| def check_is_node_in_specific_annotation( | ||
| node: ast.AST | None, | ||
| annotation_name: str, | ||
| annotation_modules: Container[str], | ||
| ) -> bool: | ||
| """ | ||
| Check is node inside specific annotation. | ||
| Checks is ast node in annotation with name `annotation_name` | ||
| and is annotation module in `annotation_modules`. | ||
| """ | ||
| if isinstance(node, ast.Subscript): | ||
| if ( | ||
| isinstance(node.value, ast.Attribute) | ||
| and isinstance(node.value.value, ast.Name) | ||
| and node.value.value.id in annotation_modules | ||
| and node.value.attr == annotation_name | ||
| ): | ||
| return True | ||
| if ( | ||
| isinstance(node.value, ast.Name) | ||
| and node.value.id in annotation_name | ||
| ): | ||
| return True | ||
| return False |
@@ -7,3 +7,12 @@ import ast | ||
| _ENUM_NAMES: Final = ( | ||
| _CONCRETE_ENUM_NAMES: Final = ( | ||
| 'enum.StrEnum', | ||
| 'StrEnum', | ||
| 'enum.IntEnum', | ||
| 'IntEnum', | ||
| 'enum.IntFlag', | ||
| 'IntFlag', | ||
| ) | ||
| _REGULAR_ENUM_NAMES: Final = ( | ||
| 'enum.Enum', | ||
@@ -15,4 +24,13 @@ 'enum.EnumType', | ||
| 'EnumMeta', | ||
| 'enum.Flag', | ||
| 'Flag', | ||
| 'enum.ReprEnum', | ||
| 'ReprEnum', | ||
| ) | ||
| _ENUM_NAMES: Final = ( | ||
| *_CONCRETE_ENUM_NAMES, | ||
| *_REGULAR_ENUM_NAMES, | ||
| ) | ||
| _ENUM_LIKE_NAMES: Final = ( | ||
@@ -37,7 +55,11 @@ *_ENUM_NAMES, | ||
| def has_enum_base(defn: ast.ClassDef) -> bool: | ||
| """Tells whether some class has `Enum` or similar class as its base.""" | ||
| return _has_one_of_base_classes(defn, _ENUM_NAMES) | ||
| def has_regular_enum_base(defn: ast.ClassDef) -> bool: | ||
| """Tells whether some class has `Enum` or similar class as its base. | ||
| Excluded `IntEnum`, `StrEnum`, `IntFlag` concrete `Enum` subclasses. | ||
| Because those classes have already been subclassed using primitives. | ||
| """ | ||
| return _has_one_of_base_classes(defn, _REGULAR_ENUM_NAMES) | ||
| def has_enum_like_base(defn: ast.ClassDef) -> bool: | ||
@@ -44,0 +66,0 @@ """ |
@@ -158,2 +158,5 @@ """ | ||
| :str:`wemake_python_styleguide.options.defaults.MAX_MATCH_CASES` | ||
| - ``max-lines-in-finally`` - maximum lines in finally block of code | ||
| defaults to | ||
| :str:`wemake_python_styleguide.options.defaults.MAX_LINES_IN_FINALLY` | ||
@@ -435,2 +438,7 @@ .. rubric:: Formatter options | ||
| ), | ||
| _Option( | ||
| '--max-lines-in-finally', | ||
| defaults.MAX_LINES_IN_FINALLY, | ||
| 'Maximum lines of expressions in a finally block.', | ||
| ), | ||
| # Formatter: | ||
@@ -437,0 +445,0 @@ _Option( |
@@ -59,2 +59,5 @@ """ | ||
| #: Maximum lines of expressions in a `finally` block. | ||
| MAX_LINES_IN_FINALLY: Final = 2 # guessed | ||
| #: Maximum number of `return` statements allowed in a single function. | ||
@@ -61,0 +64,0 @@ MAX_RETURNS: Final = 5 # 7-2 |
@@ -104,2 +104,3 @@ from typing import Any, final | ||
| max_match_cases: int = attr.ib(validator=[_min_max(min=1)]) | ||
| max_lines_in_finally: int = attr.ib(validator=[_min_max(min=1)]) | ||
| show_violation_links: bool | ||
@@ -106,0 +107,0 @@ exps_for_one_empty_line: int |
@@ -8,2 +8,3 @@ from typing import Final | ||
| classes, | ||
| complex_finally, | ||
| counts, | ||
@@ -43,2 +44,3 @@ function, | ||
| pm.MatchCasesVisitor, | ||
| complex_finally.ComplexFinallyBlocksVisitor, | ||
| ) |
@@ -17,2 +17,3 @@ from typing import Final | ||
| comments.EmptyCommentVisitor, | ||
| comments.CommentInFormattedStringVisitor, | ||
| syntax.WrongKeywordTokenVisitor, | ||
@@ -23,2 +24,3 @@ primitives.WrongNumberTokenVisitor, | ||
| conditions.IfElseVisitor, | ||
| primitives.MultilineFormattedStringTokenVisitor, | ||
| ) |
@@ -77,2 +77,3 @@ from typing import Final | ||
| subscripts.CorrectKeyVisitor, | ||
| subscripts.StrictSliceOperations, | ||
| decorators.WrongDecoratorVisitor, | ||
@@ -79,0 +80,0 @@ redundancy.RedundantEnumerateVisitor, |
@@ -66,2 +66,3 @@ """ | ||
| TooManyMatchCaseViolation | ||
| ComplexFinallyViolation | ||
@@ -113,2 +114,3 @@ Module complexity | ||
| .. autoclass:: TooManyMatchCaseViolation | ||
| .. autoclass:: ComplexFinallyViolation | ||
@@ -1402,1 +1404,30 @@ """ | ||
| code = 242 | ||
| @final | ||
| class ComplexFinallyViolation(ASTViolation): | ||
| """ | ||
| Forbids complex ``finally`` block. | ||
| Reasoning: | ||
| ``finally`` is very special. It executes code in all | ||
| cases and therefore can't fail. When there are many lines in ``finally`` | ||
| it indicates a larger problem: brittle and complex cleanups. | ||
| Solution: | ||
| Simplify the ``finally`` block. Use context managers, use ``ExitStack``. | ||
| Configuration: | ||
| This rule is configurable with ``--max-lines-in-finally``. | ||
| Default: | ||
| :str:`wemake_python_styleguide.options.defaults.MAX_LINES_IN_FINALLY` | ||
| See also: | ||
| https://peps.python.org/pep-0765 | ||
| .. versionadded:: 1.2.0 | ||
| """ | ||
| error_template = 'Found too many lines in `finally` block: {0}' | ||
| code = 243 |
@@ -12,2 +12,5 @@ import ast | ||
| from wemake_python_styleguide.logic import nodes, source, walk | ||
| from wemake_python_styleguide.logic.complexity.annotations import ( | ||
| check_is_node_in_specific_annotation, | ||
| ) | ||
| from wemake_python_styleguide.logic.tree import ( | ||
@@ -184,2 +187,8 @@ attributes, | ||
| _non_magic_modulo: ClassVar[int] = 10 | ||
| _allowed_modules_to_literal_type_hint: ClassVar[frozenset[str]] = ( | ||
| frozenset(( | ||
| 'typing', | ||
| 'typing_extensions', | ||
| )) | ||
| ) | ||
@@ -194,11 +203,16 @@ def visit_Num(self, node: ast.Constant) -> None: | ||
| parent = operators.get_parent_ignoring_unary(node) | ||
| if isinstance(parent, self._allowed_parents): | ||
| return | ||
| if node.value in constants.MAGIC_NUMBERS_WHITELIST: | ||
| return | ||
| is_non_magic = ( | ||
| isinstance(node.value, int) and node.value <= self._non_magic_modulo | ||
| ) | ||
| if isinstance(node.value, int) and node.value <= self._non_magic_modulo: | ||
| if ( | ||
| isinstance(parent, self._allowed_parents) | ||
| or node.value in constants.MAGIC_NUMBERS_WHITELIST | ||
| or is_non_magic | ||
| or check_is_node_in_specific_annotation( | ||
| parent, 'Literal', self._allowed_modules_to_literal_type_hint | ||
| ) | ||
| ): | ||
| return | ||
| try: | ||
@@ -205,0 +219,0 @@ token = self._token_dict[node.lineno, node.col_offset] |
@@ -80,3 +80,3 @@ import ast | ||
| id_attr, | ||
| ) and not enums.has_enum_base(node): | ||
| ) and not enums.has_regular_enum_base(node): | ||
| self.add_violation( | ||
@@ -83,0 +83,0 @@ oop.BuiltinSubclassViolation(node, text=id_attr), |
@@ -91,2 +91,19 @@ import ast | ||
| def _update_counters(self, node: AnyFunctionDef, sub_node: ast.AST) -> None: | ||
| """Updates statement counters for the given node.""" | ||
| error_counters: _NodeTypeHandler = { | ||
| ast.Return: self.metrics.returns, | ||
| ast.Expr: self.metrics.expressions, | ||
| ast.Await: self.metrics.awaits, | ||
| ast.Assert: self.metrics.asserts, | ||
| ast.Raise: self.metrics.raises, | ||
| } | ||
| for types, counter in error_counters.items(): | ||
| if isinstance(sub_node, types): | ||
| # We do not count returns in nested functions and classes | ||
| if types is ast.Return and get_context(sub_node) is not node: | ||
| continue | ||
| counter[node] += 1 | ||
| def _check_sub_node( | ||
@@ -103,15 +120,5 @@ self, | ||
| error_counters: _NodeTypeHandler = { | ||
| ast.Return: self.metrics.returns, | ||
| ast.Expr: self.metrics.expressions, | ||
| ast.Await: self.metrics.awaits, | ||
| ast.Assert: self.metrics.asserts, | ||
| ast.Raise: self.metrics.raises, | ||
| } | ||
| self._update_counters(node, sub_node) | ||
| for types, counter in error_counters.items(): | ||
| if isinstance(sub_node, types): | ||
| counter[node] += 1 | ||
| @final | ||
@@ -118,0 +125,0 @@ @alias( |
| import ast | ||
| from typing import Final, final | ||
| from wemake_python_styleguide.compat.constants import PY312 | ||
| from wemake_python_styleguide.logic.tree import attributes | ||
| from wemake_python_styleguide.options.validation import ValidatedOptions | ||
| from wemake_python_styleguide.types import AnyFunctionDef | ||
@@ -18,3 +20,8 @@ from wemake_python_styleguide.violations.best_practices import ( | ||
| _ALLOWED_DECORATOR_TYPES3_12: Final = ( | ||
| *_ALLOWED_DECORATOR_TYPES, | ||
| ast.Subscript, # PEP 695 - Type Parameter Syntax | ||
| ) | ||
| @final | ||
@@ -31,2 +38,14 @@ @alias( | ||
| def __init__( | ||
| self, | ||
| options: ValidatedOptions, | ||
| tree: ast.AST, | ||
| **kwargs, | ||
| ) -> None: | ||
| """Creates Decorator Visitor.""" | ||
| self.ALLOWED_DECORATOR_TYPES: Final = ( | ||
| _ALLOWED_DECORATOR_TYPES3_12 if PY312 else _ALLOWED_DECORATOR_TYPES | ||
| ) | ||
| super().__init__(options, tree, **kwargs) | ||
| def visit_any_function(self, node: AnyFunctionDef) -> None: | ||
@@ -43,3 +62,3 @@ """Checks functions' decorators.""" | ||
| def _is_allowed_decorator(self, node: ast.expr) -> bool: | ||
| if not isinstance(node, _ALLOWED_DECORATOR_TYPES): | ||
| if not isinstance(node, self.ALLOWED_DECORATOR_TYPES): | ||
| return False | ||
@@ -50,5 +69,4 @@ | ||
| return all( | ||
| isinstance(part, _ALLOWED_DECORATOR_TYPES) | ||
| for part in attributes.parts(node) | ||
| return attributes.only_consists_of_parts( | ||
| node, self.ALLOWED_DECORATOR_TYPES | ||
| ) |
@@ -258,10 +258,31 @@ import ast | ||
| def _is_node_in_loop_iter(self, node: ast.AST, loop_iter: ast.expr) -> bool: | ||
| return walk.is_contained_by(node, loop_iter) or node is loop_iter | ||
| def _check_await_inside_loop(self, node: ast.Await) -> None: | ||
| node_parent = walk.get_closest_parent(node, self._forbidden_await_loops) | ||
| if isinstance(node_parent, AnyComprehension) and all( | ||
| comprehension.is_async for comprehension in node_parent.generators | ||
| if node_parent is None: | ||
| return | ||
| if isinstance(node_parent, AnyComprehension): | ||
| if all( | ||
| comprehension.is_async | ||
| for comprehension in node_parent.generators | ||
| ): | ||
| # async comprehensions are allowed to use `await` | ||
| return | ||
| if any( | ||
| self._is_node_in_loop_iter(node, gen.iter) | ||
| for gen in node_parent.generators | ||
| ): | ||
| return | ||
| if isinstance(node_parent, ast.For) and self._is_node_in_loop_iter( | ||
| node, node_parent.iter | ||
| ): | ||
| # async comprehensions are allowed to use `await` | ||
| # await allowed in loop definition | ||
| return | ||
| if node_parent is not None: | ||
| self.add_violation(AwaitInLoopViolation(node)) | ||
| self.add_violation(AwaitInLoopViolation(node)) |
@@ -60,2 +60,3 @@ import ast | ||
| indexes: list[ast.expr | None] = [] | ||
| lower_ok = node.slice.lower is None or ( | ||
@@ -65,7 +66,5 @@ not self._is_zero(node.slice.lower) | ||
| ) | ||
| upper_ok = node.slice.upper is None or not self._is_none( | ||
| node.slice.upper, | ||
| ) | ||
| step_ok = node.slice.step is None or ( | ||
@@ -76,9 +75,14 @@ not self._is_one(node.slice.step) | ||
| if not (lower_ok and upper_ok and step_ok): | ||
| self.add_violation( | ||
| consistency.RedundantSubscriptViolation( | ||
| node, | ||
| ), | ||
| ) | ||
| if not lower_ok: | ||
| indexes.append(node.slice.lower) | ||
| if not upper_ok: | ||
| indexes.append(node.slice.upper) | ||
| if not step_ok: | ||
| indexes.append(node.slice.step) | ||
| for index in indexes: | ||
| self.add_violation(consistency.RedundantSubscriptViolation(index)) | ||
| def _check_slice_assignment(self, node: ast.Subscript) -> None: | ||
@@ -188,1 +192,67 @@ if not isinstance(node.ctx, ast.Store): | ||
| ) | ||
| @final | ||
| class StrictSliceOperations(base.BaseNodeVisitor): | ||
| """Check for stricter operation with slices.""" | ||
| def visit_Slice(self, node: ast.Slice) -> None: | ||
| """Visit slice.""" | ||
| self._check_reverse(node) | ||
| self._check_copy(node) | ||
| self.generic_visit(node) | ||
| def _check_reverse(self, node: ast.Slice) -> None: | ||
| if not ( | ||
| self._is_node_or_none(node.lower) | ||
| or self._is_node_have_value(node.lower, value_to_check=-1) | ||
| ): | ||
| return | ||
| if not ( | ||
| self._is_node_or_none(node.upper) | ||
| and self._is_node_have_value(node.step, value_to_check=-1) | ||
| ): | ||
| return | ||
| self.add_violation( | ||
| best_practices.NonStrictSliceOperationsViolation(node) | ||
| ) | ||
| def _check_copy(self, node: ast.Slice) -> None: | ||
| if not ( | ||
| self._is_node_or_none(node.lower) | ||
| or self._is_node_have_value(node.lower, value_to_check=0) | ||
| ): | ||
| return | ||
| if not self._is_node_or_none(node.upper): | ||
| return | ||
| if not ( | ||
| self._is_node_or_none(node.step) | ||
| or self._is_node_have_value(node.step, value_to_check=1) | ||
| ): | ||
| return | ||
| self.add_violation( | ||
| best_practices.NonStrictSliceOperationsViolation(node) | ||
| ) | ||
| def _is_node_or_none(self, node: ast.AST | None) -> bool: | ||
| return node is None or ( | ||
| isinstance(node, ast.Constant) and node.value is None | ||
| ) | ||
| def _is_node_have_value( | ||
| self, node: ast.AST | None, value_to_check: int | ||
| ) -> bool: | ||
| if value_to_check < 0: | ||
| return ( | ||
| isinstance(node, ast.UnaryOp) | ||
| and isinstance(node.op, ast.USub) | ||
| and isinstance(node.operand, ast.Constant) | ||
| and node.operand.value == abs(value_to_check) | ||
| ) | ||
| return isinstance(node, ast.Constant) and node.value == value_to_check |
@@ -29,2 +29,3 @@ r""" | ||
| from wemake_python_styleguide.violations.best_practices import ( | ||
| CommentInFormattedStringViolation, | ||
| EmptyCommentViolation, | ||
@@ -315,1 +316,31 @@ ForbiddenInlineIgnoreViolation, | ||
| ) | ||
| @final | ||
| class CommentInFormattedStringVisitor(BaseTokenVisitor): # pragma: >=3.12 cover | ||
| """Checks comment in formatted strings.""" | ||
| _comment_in_fstring: ClassVar[re.Pattern[str]] = re.compile( | ||
| r""" | ||
| .* # (1) anything before the f-string | ||
| fr?(['"]) # (2) `f` or `fr`prefix + a single or double quote | ||
| .* # (3) any characters up to… | ||
| \{ # (4) opening brace | ||
| [^}]* # (5) any characters except closing braces | ||
| # # (6) hash symbol | ||
| [^}]*\n # (7) chars up to a newline (i.e. multiline) | ||
| """, | ||
| re.VERBOSE, | ||
| ) | ||
| def visit_fstring_start(self, token: tokenize.TokenInfo) -> None: | ||
| """Preforms fstring check.""" | ||
| self._check_is_fstring_ends_with_comment(token) | ||
| def _check_is_fstring_ends_with_comment( | ||
| self, | ||
| token: tokenize.TokenInfo, | ||
| ) -> None: | ||
| """Checks is formatted string ends with comment.""" | ||
| if self._comment_in_fstring.match(token.line): | ||
| self.add_violation(CommentInFormattedStringViolation(token)) |
@@ -15,2 +15,3 @@ import re | ||
| from wemake_python_styleguide.violations.best_practices import ( | ||
| MultilineFormattedStringViolation, | ||
| WrongUnicodeEscapeViolation, | ||
@@ -230,1 +231,31 @@ ) | ||
| self._checker.check_string_modifiers(token, modifiers) | ||
| @final | ||
| class MultilineFormattedStringTokenVisitor( | ||
| BaseTokenVisitor | ||
| ): # pragma: >=3.12 cover | ||
| """Checks incorrect formatted string usages.""" | ||
| _multiline_fstring_pattern: ClassVar[re.Pattern[str]] = re.compile( | ||
| r""" | ||
| .* # (1) anything before the f-string | ||
| fr?(['"]) # (2) `f` or `fr`prefix + a single or double quote | ||
| (?!\1\1) # (3) not triple quote | ||
| .* # (4) any characters up to… | ||
| (\{.*\}.)* # (5) any fully closed {…} expressions, if present | ||
| .* # (6) then more arbitrary chars | ||
| \{ # (7) an opening brace of an f-expr | ||
| [^}]*\n # (8) chars up to a newline (i.e. multiline) | ||
| """, | ||
| re.VERBOSE, | ||
| ) | ||
| def visit_fstring_start(self, token: tokenize.TokenInfo) -> None: | ||
| """Performs check.""" | ||
| self._check_fstring_is_multi_lined(token) | ||
| def _check_fstring_is_multi_lined(self, token: tokenize.TokenInfo) -> None: | ||
| """Finds if f-string is multi-line.""" | ||
| if self._multiline_fstring_pattern.match(token.line): | ||
| self.add_violation(MultilineFormattedStringViolation(token)) |
Sorry, the diff of this file is too big to display
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
687775
1.9%161
0.63%18092
1.94%