|
| 1 | +import ast |
| 2 | + |
| 3 | +from vermin.fstring_detector import FStringDetector |
| 4 | + |
| 5 | +from .testutils import VerminTest |
| 6 | + |
| 7 | + |
| 8 | +def joined_strs(source): |
| 9 | + tree = ast.parse(source) |
| 10 | + return [n for n in ast.walk(tree) if isinstance(n, ast.JoinedStr)] |
| 11 | + |
| 12 | + |
| 13 | +class FStringDetectorTests(VerminTest): |
| 14 | + def assert_self_doc(self, source, expected): |
| 15 | + detector = FStringDetector(source) |
| 16 | + nodes = joined_strs(source) |
| 17 | + self.assertTrue(nodes, "no f-string in: " + source) |
| 18 | + self.assertEqual(expected, any(detector.is_self_doc(n) for n in nodes), source) |
| 19 | + |
| 20 | + def assert_pep701(self, source, expected): |
| 21 | + """Assert the source's f-strings trigger the named PEP 701 violation. `None` expectation means |
| 22 | + "no violation", not "no f-string", which is asserted separately. |
| 23 | + """ |
| 24 | + detector = FStringDetector(source) |
| 25 | + nodes = joined_strs(source) |
| 26 | + self.assertTrue(nodes, "no f-string in: " + source) |
| 27 | + if expected is None: |
| 28 | + self.assertTrue(all(detector.pep701_violation(n) is None for n in nodes), source) |
| 29 | + else: |
| 30 | + self.assertTrue(any(detector.pep701_violation(n) == expected for n in nodes), source) |
| 31 | + |
| 32 | + def test_none_source(self): |
| 33 | + detector = FStringDetector(None) |
| 34 | + node = joined_strs('f"{x}"')[0] |
| 35 | + self.assertFalse(detector.is_self_doc(node)) |
| 36 | + self.assertIsNone(detector.pep701_violation(node)) |
| 37 | + # pylint: disable=W0212 |
| 38 | + self.assertIsNone(detector._read_fstring_token(node, None)) |
| 39 | + |
| 40 | + def test_input_guards(self): |
| 41 | + detector = FStringDetector('f"{x}"') |
| 42 | + bare = ast.JoinedStr() |
| 43 | + const = ast.Constant(value="a") |
| 44 | + self.assertIsNone(detector.pep701_violation(bare)) |
| 45 | + self.assertFalse(detector.is_self_doc(const)) |
| 46 | + self.assertIsNone(detector.pep701_violation(const)) |
| 47 | + # pylint: disable=W0212 |
| 48 | + self.assertIsNone(detector._read_fstring_token(bare, 'f"{x}"'.splitlines())) |
| 49 | + self.assertIsNone(detector._read_fstring_token(bare, [])) |
| 50 | + |
| 51 | + @VerminTest.skipUnlessVersion(3, 8) |
| 52 | + @VerminTest.parameterized_args([ |
| 53 | + ("f'{a=}'", True), |
| 54 | + ("f'hello {name=}'", True), |
| 55 | + ("f'{b=}={a}'", True), |
| 56 | + ("f'{a =}'", True), |
| 57 | + ("f'{ a=}'", True), |
| 58 | + ("f'{a= }'", True), |
| 59 | + ("f'{ a = }'", True), |
| 60 | + ("f'{1+1=}'", True), |
| 61 | + ("f'{a+b=}'", True), |
| 62 | + ("f'{-5=}'", True), |
| 63 | + ("f'{(1,2,3)=}'", True), |
| 64 | + ("f'{ {1,2,3}=}'", True), |
| 65 | + ("f'{[x for x in [1,2,3]]=}'", True), |
| 66 | + ("f'{0==1=}'", True), |
| 67 | + ("f'{3.14=:10.10}'", True), |
| 68 | + ("f'{3.14=!s:10.10}'", True), |
| 69 | + ("f'{x=!r}'", True), |
| 70 | + ("f'{x=:.2f}'", True), |
| 71 | + ("f'{f\"{3.1415=:.1f}\":*^20}'", True), |
| 72 | + ("f'{{literal}}{a=}'", True), |
| 73 | + ("f'''{\n3\n=}'''", True), |
| 74 | + ("f'{f(a=4)=}'", True), |
| 75 | + ("f\"\\N{EXCLAMATION MARK}{a=}\"", True), |
| 76 | +
|
| 77 | + # CRLFs must not shift the byte offsets of nodes on later lines. |
| 78 | + ("x = 1\r\ns = f\"{x=}\"\r\n", True), |
| 79 | + ("x = 1\r\ns = f\"{x=!r}\"\r\n", True), |
| 80 | +
|
| 81 | + # Bare `\r` endings and parser-only separators, like `\v` and `\f`, in string literals must not |
| 82 | + # shift offsets either. |
| 83 | + ("x = 1\rs = f\"{x=}\"\r", True), |
| 84 | + ("y = \"\v\f\"\ns = f\"{x=}\"\n", True), |
| 85 | +
|
| 86 | + ("a = 1\nf'a={a}'", False), |
| 87 | + ("a = 1\nf'={a}'", False), |
| 88 | + ("f'{a != b}'", False), |
| 89 | + ("f'{a == b}'", False), |
| 90 | + ("f'{a >= b}'", False), |
| 91 | + ("f'{a <= b}'", False), |
| 92 | + ("f'{x:=10}'", False), |
| 93 | + ("f'hello {name!r}'", False), |
| 94 | + ("f'{x!r}'", False), |
| 95 | + ("f'{x!s}'", False), |
| 96 | + ("f'{x!a}'", False), |
| 97 | + ("f'{x:.2f}'", False), |
| 98 | + ("f'val={val:.2f}'", False), |
| 99 | + ("f'{(a+b)}={x!r}'", False), |
| 100 | + ("f'{{x={a}}}'", False), |
| 101 | + ("f'{x}'", False), |
| 102 | + ("f'{x:{width}}'", False), |
| 103 | + ("f'={cos(radians(theta)):.3f}'", False), |
| 104 | + ("x = 1\r\ns = f\"{x+1}\"\r\n", False), |
| 105 | + ("x = 1\rs = f\"{x+1}\"\r", False), |
| 106 | + ]) |
| 107 | + def test_self_doc(self, source, expected): |
| 108 | + self.assert_self_doc(source, expected) |
| 109 | + |
| 110 | + @VerminTest.skipUnlessVersion(3, 12) |
| 111 | + @VerminTest.parameterized_args([ |
| 112 | + ('f"{\n1+2\n}"', "multi_line"), |
| 113 | + ('f"{\n x +\n y\n}"', "multi_line"), |
| 114 | + ("f\"{\n'''a'''\n}\"", "multi_line"), |
| 115 | + ("'''lit''' f\"{x\n+y}\"", "multi_line"), |
| 116 | + ('f"{a \\\n}"', "multi_line"), |
| 117 | +
|
| 118 | + ('f"{x}"', None), |
| 119 | + ('f"{x:10}"', None), |
| 120 | + ("f'''{\n3\n}'''", None), |
| 121 | + ('f"\\N{EXCLAMATION MARK}{x}"', None), |
| 122 | + ('s = (f"got {x}: "\n f"done")', None), |
| 123 | + ('s = f"{a}" f"{b}"', None), |
| 124 | + ('s = f"lit {a}" f"""{b\n+ c}"""', None), |
| 125 | + ('s = (f"got {x}: "\n f"""done\n{p\n+ q}""")', None), |
| 126 | + ]) |
| 127 | + def test_pep701_multi_line(self, source, expected): |
| 128 | + self.assert_pep701(source, expected) |
| 129 | + |
| 130 | + @VerminTest.skipUnlessVersion(3, 12) |
| 131 | + @VerminTest.parameterized_args([ |
| 132 | + ('f"outer {f"inner"}"', "nested_same_quote"), |
| 133 | + ("f'outer {f'inner'}'", "nested_same_quote"), |
| 134 | + ('f"""outer {f"""inner"""}"""', "nested_same_quote"), |
| 135 | + ('f"outer {f"""inner"""}"', "nested_same_quote"), |
| 136 | + ('f"{x:{f".2f"}}"', "nested_same_quote"), |
| 137 | + ('f"1 {f"2 {f"3"}"}"', "nested_same_quote"), |
| 138 | +
|
| 139 | + ('f"""outer {f"inner"}"""', None), |
| 140 | + ('f"outer {f\'inner\'}"', None), |
| 141 | + ("f'outer {f\"inner\"}'", None), |
| 142 | + ]) |
| 143 | + def test_pep701_nested_same_quote(self, source, expected): |
| 144 | + self.assert_pep701(source, expected) |
| 145 | + |
| 146 | + @VerminTest.skipUnlessVersion(3, 12) |
| 147 | + @VerminTest.parameterized_args([ |
| 148 | + ("'lit' f\"{f\"{x}\"}\"", "nested_same_quote"), |
| 149 | + ('"""lit""" f"{f"{x}"}"', "nested_same_quote"), |
| 150 | + ("'''lit''' f\"{f\"{x}\"}\"", "nested_same_quote"), |
| 151 | + ("f'{\"lit\" f'{x}'}'", "nested_same_quote"), |
| 152 | + ('f"""{rf"""{x}"""}"""', "nested_same_quote"), |
| 153 | + ('f"{fr"""{x}"""}"', "nested_same_quote"), |
| 154 | + ("f'outer {rf'''{x}'''}'", "nested_same_quote"), |
| 155 | + ('f"{F"e{fr"""{x}"""}config"}"', "nested_same_quote"), |
| 156 | + ("('lit'\n# comment between\n f\"{f\"{x}\"}\")", "nested_same_quote"), |
| 157 | +
|
| 158 | + ("'pre' f\"{f'{x}'}\"", None), |
| 159 | + ("'''pre''' f\"{f'{x}'}\"", None), |
| 160 | +
|
| 161 | + # Format-spec nested f-strings are valid pre-3.12 even when same-quoted. |
| 162 | + ("f\"{x:{f'{y}'}}\"", None), |
| 163 | + ('f"""{x:{f"{y}"}}"""', None), |
| 164 | + ]) |
| 165 | + def test_pep701_nested_same_quote_implicit_concat(self, source, expected): |
| 166 | + self.assert_pep701(source, expected) |
| 167 | + |
| 168 | + @VerminTest.skipUnlessVersion(3, 12) |
| 169 | + @VerminTest.parameterized_args([ |
| 170 | + ("f\"{'\\n'.join(x)}\"", "backslash"), |
| 171 | + ("f\"{r'\\n'}\"", "backslash"), |
| 172 | +
|
| 173 | + ("f\"{chr(10)}\"", None), |
| 174 | + ("f'hello\\nworld'", None), |
| 175 | + ]) |
| 176 | + def test_pep701_backslash(self, source, expected): |
| 177 | + self.assert_pep701(source, expected) |
| 178 | + |
| 179 | + @VerminTest.skipUnlessVersion(3, 12) |
| 180 | + def test_pep701_backslash_line_continuation(self): |
| 181 | + source = 'f"{a \\\n}"' |
| 182 | + node = joined_strs(source)[0].values[0] |
| 183 | + detector = FStringDetector(source) |
| 184 | + # pylint: disable=W0212 |
| 185 | + self.assertTrue(detector._has_pep701_backslash(node, source.splitlines())) |
| 186 | + |
| 187 | + @VerminTest.skipUnlessVersion(3, 12) |
| 188 | + @VerminTest.parameterized_args([ |
| 189 | + ('f"""{x # comment\n}"""', "comment"), |
| 190 | + ('f"""{x\n# comment\n}"""', "comment"), |
| 191 | +
|
| 192 | + ("f\"{'#notacomment'}\"", None), |
| 193 | + ('f"{x}"', None), |
| 194 | +
|
| 195 | + # `#` in a format spec is spec filler text, valid since 3.6. |
| 196 | + ('f"""{x:>3 # c\n}"""', None), |
| 197 | + ('f"""{x:{w} # c\n}"""', None), |
| 198 | + ('f"""{x!r:>3 # c\n}"""', None), |
| 199 | +
|
| 200 | + # A comment before the format spec is a PEP 701 comment. |
| 201 | + ('f"""{x # c\n:>3}"""', "comment"), |
| 202 | + ]) |
| 203 | + def test_pep701_comment(self, source, expected): |
| 204 | + self.assert_pep701(source, expected) |
| 205 | + |
| 206 | + @VerminTest.skipUnlessVersion(3, 12) |
| 207 | + @VerminTest.parameterized_args([ |
| 208 | + ("('lit' f\"{x}\")", ('"', False)), |
| 209 | + ("('''lit''' f'{x}')", ("'", False)), |
| 210 | + ('(f"lit {a}" f"""{b\n+ c}""")', ('"', False)), |
| 211 | + ('f"{f"{x}"}"', ('"', False)), |
| 212 | + ('f"""{rf"""{x}"""}"""', ('"', True)), |
| 213 | + ('rf"raw {x}"', ('"', False)), |
| 214 | + ("(f'lit' f\"{x}\")", ("'", False)), |
| 215 | + ]) |
| 216 | + def test_read_fstring_token(self, source, expected): |
| 217 | + detector = FStringDetector(source) |
| 218 | + nodes = joined_strs(source) |
| 219 | + self.assertTrue(nodes, "no f-string in: " + source) |
| 220 | + # pylint: disable=W0212 |
| 221 | + got = detector._read_fstring_token(nodes[0], source.splitlines()) |
| 222 | + self.assertEqual(expected, got, source) |
| 223 | + |
| 224 | + @VerminTest.skipUnlessVersion(3, 12) |
| 225 | + @VerminTest.parameterized_args([ |
| 226 | + ('f"""{x:>3 # c\n}"""', False), |
| 227 | + ('f"""{x:{w} # c\n}"""', False), |
| 228 | + ('f"""{x!r:>3 # c\n}"""', False), |
| 229 | + ('f"""{x # c\n:>3}"""', True), |
| 230 | + ('f"""{x\n# comment\n}"""', True), |
| 231 | + ("f\"{'#notacomment'}\"", False), |
| 232 | + ]) |
| 233 | + def test_pep701_comment_region(self, source, expected): |
| 234 | + detector = FStringDetector(source) |
| 235 | + node = joined_strs(source)[0].values[0] |
| 236 | + # pylint: disable=W0212 |
| 237 | + got = detector._has_pep701_comment(node, source.splitlines()) |
| 238 | + self.assertEqual(expected, got, source) |
| 239 | + |
| 240 | + @VerminTest.skipUnlessVersion(3, 12) |
| 241 | + @VerminTest.parameterized_args([ |
| 242 | + ("x = 1\r\ns = f\"{f\"{x}\"}\"\r\n", "nested_same_quote"), |
| 243 | + ("x = 1\r\ns = f\"{\r\nx\r\n}\"\r\n", "multi_line"), |
| 244 | + ("x = 1\r\ns = f\"{x # c\r\n}\"\r\n", "multi_line"), |
| 245 | + ("x = 1\r\ns = f\"\"\"{x # c\r\n}\"\"\"\r\n", "comment"), |
| 246 | + ("x = 1\r\n'''lit''' f\"{x\n+y}\"\r\n", "multi_line"), |
| 247 | + ("x = 1\r\ns = f\"{x!r}\"\r\n", None), |
| 248 | + ]) |
| 249 | + def test_pep701_crlf(self, source, expected): |
| 250 | + self.assert_pep701(source, expected) |
| 251 | + |
| 252 | + @VerminTest.skipUnlessVersion(3, 12) |
| 253 | + @VerminTest.parameterized_args([ |
| 254 | + # Bare `\r` endings and parser-only separators, like `\v` and `\f`, in string literals must not |
| 255 | + # shift offsets of nodes on later lines. |
| 256 | + ("x = 1\rs = f\"{f\"{x}\"}\"\r", "nested_same_quote"), |
| 257 | + ("y = \"\v\f\"\ns = f\"{f\"{x}\"}\"\n", "nested_same_quote"), |
| 258 | + ("x = 1\rs = f\"{x}\"\r", None), |
| 259 | + ("y = \"\v\f\"\ns = f\"{x}\"\n", None), |
| 260 | + ]) |
| 261 | + def test_pep701_parser_line_breaks(self, source, expected): |
| 262 | + self.assert_pep701(source, expected) |
0 commit comments