Skip to content

Commit 1d95728

Browse files
mkaraevsobolevn
andauthored
Fixes wemake-services#3405, forbids miltiline fstrings (wemake-services#3411)
Co-authored-by: sobolevn <mail@sobolevn.me>
1 parent b6434f6 commit 1d95728

File tree

6 files changed

+156
-0
lines changed

6 files changed

+156
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Semantic versioning in our case means:
2222
### Features
2323

2424
- Adds `WPS478`: forbids using non strict slice operations, #1011
25+
- Adds `WPS479`: forbids using multiline fstrings, #3405
2526

2627
### Bugfixes
2728

tests/test_checker/test_noqa.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@
243243
'WPS476': 1,
244244
'WPS477': 0, # enabled only in python 3.13+
245245
'WPS478': 1,
246+
'WPS479': 0,
246247
'WPS500': 1,
247248
'WPS501': 1,
248249
'WPS502': 0, # disabled since 1.0.0
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import pytest
2+
3+
from wemake_python_styleguide.compat.constants import PY312
4+
from wemake_python_styleguide.violations.best_practices import (
5+
MultilineFormattedStringViolation,
6+
)
7+
from wemake_python_styleguide.visitors.tokenize.primitives import (
8+
MultilineFormattedStringTokenVisitor,
9+
)
10+
11+
if not PY312: # pragma: >=3.12 no cover
12+
pytest.skip(
13+
reason='unterminated string literal was added in 3.12',
14+
allow_module_level=True,
15+
)
16+
17+
# Wrong:
18+
single_quote_formatted_string_wrong = """x=f'{ 1
19+
}'
20+
"""
21+
22+
fr_prefix_formatted_string_wrong = """x=fr'{1
23+
}'"""
24+
25+
double_quote_formatted_string_wrong = """x=f" {2} { 1
26+
}"
27+
"""
28+
29+
# Correct:
30+
triple_quote_formatted_string_first_correct = """x=f'''{ 1
31+
}'''
32+
"""
33+
34+
triple_quote_formatted_string_second_correct = '''x=f"""{ 1
35+
}"""
36+
'''
37+
single_line_formatted_string = (
38+
"""formatted_string_complex = f'1+1={1 + 1}' # noqa: WPS237"""
39+
)
40+
41+
fr_prefix_formatted_string = (
42+
"""formatted_string_complex = fr'1+1={1 + 1}' # noqa: WPS237"""
43+
)
44+
45+
46+
@pytest.mark.parametrize(
47+
'code',
48+
[
49+
triple_quote_formatted_string_first_correct,
50+
triple_quote_formatted_string_second_correct,
51+
single_line_formatted_string,
52+
fr_prefix_formatted_string,
53+
],
54+
)
55+
def test_correctly_formatted_string(
56+
parse_tokens,
57+
assert_errors,
58+
default_options,
59+
code,
60+
):
61+
"""Ensures that correct formatting works."""
62+
tokens = parse_tokens(code)
63+
visitor = MultilineFormattedStringTokenVisitor(
64+
default_options, file_tokens=tokens
65+
)
66+
visitor.run()
67+
assert_errors(visitor, [])
68+
69+
70+
@pytest.mark.parametrize(
71+
'code',
72+
[
73+
single_quote_formatted_string_wrong,
74+
double_quote_formatted_string_wrong,
75+
fr_prefix_formatted_string_wrong,
76+
],
77+
)
78+
def test_incorrectly_formatted_string(
79+
parse_tokens,
80+
assert_errors,
81+
default_options,
82+
code,
83+
):
84+
"""Ensures that correct formatting works."""
85+
tokens = parse_tokens(code)
86+
visitor = MultilineFormattedStringTokenVisitor(
87+
default_options, file_tokens=tokens
88+
)
89+
visitor.run()
90+
assert_errors(visitor, [MultilineFormattedStringViolation])

wemake_python_styleguide/presets/types/file_tokens.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,5 @@
1919
primitives.WrongStringTokenVisitor,
2020
statements.MultilineStringVisitor,
2121
conditions.IfElseVisitor,
22+
primitives.MultilineFormattedStringTokenVisitor,
2223
)

wemake_python_styleguide/violations/best_practices.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@
9595
AwaitInLoopViolation
9696
SneakyTypeVarWithDefaultViolation
9797
NonStrictSliceOperationsViolation
98+
MultilineFormattedStringViolation
9899
99100
Best practices
100101
--------------
@@ -178,6 +179,7 @@
178179
.. autoclass:: AwaitInLoopViolation
179180
.. autoclass:: SneakyTypeVarWithDefaultViolation
180181
.. autoclass:: NonStrictSliceOperationsViolation
182+
.. autoclass:: MultilineFormattedStringViolation
181183
182184
"""
183185

@@ -3074,3 +3076,33 @@ class NonStrictSliceOperationsViolation(ASTViolation):
30743076

30753077
error_template = 'Found non strict slice operation'
30763078
code = 478
3079+
3080+
3081+
@final
3082+
class MultilineFormattedStringViolation(TokenizeViolation):
3083+
"""
3084+
Forbid using multi-line formatted string with single and double quotes.
3085+
3086+
Reasoning:
3087+
Multiline f-strings must use triple quotes for clarity.
3088+
Single f-strings may not span lines.
3089+
3090+
Solution:
3091+
Use triple quotes instead of single quotes.
3092+
3093+
Example::
3094+
3095+
# Correct:
3096+
x = f''' { 1
3097+
...}'''
3098+
3099+
# Wrong:
3100+
x = f' { 1
3101+
...}'
3102+
3103+
.. versionadded:: 1.2.0
3104+
3105+
"""
3106+
3107+
error_template = 'Found multi-line formatted string'
3108+
code = 479

wemake_python_styleguide/visitors/tokenize/primitives.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from wemake_python_styleguide.violations import consistency
1313
from wemake_python_styleguide.violations.base import TokenizeViolation
1414
from wemake_python_styleguide.violations.best_practices import (
15+
MultilineFormattedStringViolation,
1516
WrongUnicodeEscapeViolation,
1617
)
1718
from wemake_python_styleguide.visitors.base import BaseTokenVisitor
@@ -226,3 +227,33 @@ def visit_fstring_start( # pragma: >3.12 cover
226227
# but, since we don't recommend `f`-string, this is a low-priority
227228
modifiers = token.string[:-1]
228229
self._checker.check_string_modifiers(token, modifiers)
230+
231+
232+
@final
233+
class MultilineFormattedStringTokenVisitor(
234+
BaseTokenVisitor
235+
): # pragma: >=3.12 cover
236+
"""Checks incorrect formatted string usages."""
237+
238+
_multiline_fstring_pattern: ClassVar[re.Pattern[str]] = re.compile(
239+
r"""
240+
.* # (1) anything before the f-string
241+
fr?(['"]) # (2) `f` or `fr`prefix + a single or double quote
242+
(?!\1\1) # (3) not triple quote
243+
.* # (4) any characters up to…
244+
(\{.*\}.)* # (5) any fully closed {…} expressions, if present
245+
.* # (6) then more arbitrary chars
246+
\{ # (7) an opening brace of an f-expr
247+
[^}]*\n # (8) chars up to a newline (i.e. multiline)
248+
""",
249+
re.VERBOSE,
250+
)
251+
252+
def visit_fstring_start(self, token: tokenize.TokenInfo) -> None:
253+
"""Performs check."""
254+
self._check_fstring_is_multi_lined(token)
255+
256+
def _check_fstring_is_multi_lined(self, token: tokenize.TokenInfo) -> None:
257+
"""Finds if f-string is multi-line."""
258+
if self._multiline_fstring_pattern.match(token.line):
259+
self.add_violation(MultilineFormattedStringViolation(token))

0 commit comments

Comments
 (0)