|
8 | 8 | import token |
9 | 9 | import tokenize |
10 | 10 | import unittest |
| 11 | +import warnings |
| 12 | +import weakref |
11 | 13 | from io import BytesIO, StringIO |
12 | 14 | from textwrap import dedent |
13 | 15 | from unittest import TestCase, mock |
@@ -2244,6 +2246,42 @@ def _get_tokens(source, *, extra_tokens=False): |
2244 | 2246 | extra_tokens=extra_tokens, |
2245 | 2247 | )) |
2246 | 2248 |
|
| 2249 | + def test_readline_reentry_is_rejected_without_leaking_cycle(self): |
| 2250 | + def make_cycle(): |
| 2251 | + iterator = None |
| 2252 | + |
| 2253 | + def readline(): |
| 2254 | + next(iterator) |
| 2255 | + |
| 2256 | + iterator = _tokenize.TokenizerIter(readline, extra_tokens=False) |
| 2257 | + with self.assertRaisesRegex( |
| 2258 | + RuntimeError, "^tokenizer is already executing$" |
| 2259 | + ): |
| 2260 | + next(iterator) |
| 2261 | + return weakref.ref(readline) |
| 2262 | + |
| 2263 | + readline_ref = make_cycle() |
| 2264 | + support.gc_collect() |
| 2265 | + self.assertIsNone(readline_ref()) |
| 2266 | + |
| 2267 | + def test_warning_reentry_is_rejected(self): |
| 2268 | + iterator = None |
| 2269 | + |
| 2270 | + def showwarning(*args, **kwargs): |
| 2271 | + next(iterator) |
| 2272 | + |
| 2273 | + with warnings.catch_warnings(): |
| 2274 | + warnings.simplefilter("always") |
| 2275 | + with mock.patch.object(warnings, "showwarning", showwarning): |
| 2276 | + iterator = _tokenize.TokenizerIter( |
| 2277 | + StringIO("1if\n").readline, |
| 2278 | + extra_tokens=False, |
| 2279 | + ) |
| 2280 | + with self.assertRaisesRegex( |
| 2281 | + RuntimeError, "^tokenizer is already executing$" |
| 2282 | + ): |
| 2283 | + next(iterator) |
| 2284 | + |
2247 | 2285 | def check_tokenize(self, s, expected): |
2248 | 2286 | # Format the tokens in s in a table format. |
2249 | 2287 | # The ENDMARKER and final NEWLINE are omitted. |
@@ -2427,6 +2465,47 @@ def test_stop_iteration_skips_encoded_readline_codec_lookup(self): |
2427 | 2465 | (token.ENDMARKER, "", (1, 0), (1, 0), ""), |
2428 | 2466 | ) |
2429 | 2467 |
|
| 2468 | + def test_fstring_offsets_remain_valid_after_source_reallocation(self): |
| 2469 | + padding = " " * 9000 |
| 2470 | + expression_line = ")=:>{2}}\n" |
| 2471 | + lines = iter([ |
| 2472 | + 'f"""\n', |
| 2473 | + "{(\n", |
| 2474 | + padding + "1\n", |
| 2475 | + expression_line, |
| 2476 | + '"""\n', |
| 2477 | + "", |
| 2478 | + ]) |
| 2479 | + tokens = list(tokenize._generate_tokens_from_c_tokenizer( |
| 2480 | + lines.__next__, |
| 2481 | + extra_tokens=True, |
| 2482 | + )) |
| 2483 | + self.assertEqual(tokens, [ |
| 2484 | + tokenize.TokenInfo(token.FSTRING_START, 'f"""', (1, 0), (1, 4), 'f"""\n'), |
| 2485 | + tokenize.TokenInfo(token.FSTRING_MIDDLE, "\n", (1, 4), (2, 0), 'f"""\n{(\n'), |
| 2486 | + tokenize.TokenInfo(token.OP, "{", (2, 0), (2, 1), "{(\n"), |
| 2487 | + tokenize.TokenInfo(token.OP, "(", (2, 1), (2, 2), "{(\n"), |
| 2488 | + tokenize.TokenInfo(token.NL, "\n", (2, 2), (2, 3), "{(\n"), |
| 2489 | + tokenize.TokenInfo(token.NUMBER, "1", (3, 9000), (3, 9001), padding + "1\n"), |
| 2490 | + tokenize.TokenInfo(token.NL, "\n", (3, 9001), (3, 9002), padding + "1\n"), |
| 2491 | + tokenize.TokenInfo(token.OP, ")", (4, 0), (4, 1), expression_line), |
| 2492 | + tokenize.TokenInfo(token.OP, "=", (4, 1), (4, 2), expression_line), |
| 2493 | + tokenize.TokenInfo(token.OP, ":", (4, 2), (4, 3), expression_line), |
| 2494 | + tokenize.TokenInfo(token.FSTRING_MIDDLE, ">", (4, 3), (4, 4), expression_line), |
| 2495 | + tokenize.TokenInfo(token.OP, "{", (4, 4), (4, 5), expression_line), |
| 2496 | + tokenize.TokenInfo(token.NUMBER, "2", (4, 5), (4, 6), expression_line), |
| 2497 | + tokenize.TokenInfo(token.OP, "}", (4, 6), (4, 7), expression_line), |
| 2498 | + tokenize.TokenInfo(token.FSTRING_MIDDLE, "", (4, 7), (4, 7), expression_line), |
| 2499 | + tokenize.TokenInfo(token.OP, "}", (4, 7), (4, 8), expression_line), |
| 2500 | + tokenize.TokenInfo( |
| 2501 | + token.FSTRING_MIDDLE, "\n", (4, 8), (5, 0), |
| 2502 | + expression_line + '"""\n', |
| 2503 | + ), |
| 2504 | + tokenize.TokenInfo(token.FSTRING_END, '"""', (5, 0), (5, 3), '"""\n'), |
| 2505 | + tokenize.TokenInfo(token.NEWLINE, "\n", (5, 3), (5, 4), '"""\n'), |
| 2506 | + tokenize.TokenInfo(token.ENDMARKER, "", (6, 0), (6, 0), ""), |
| 2507 | + ]) |
| 2508 | + |
2430 | 2509 | def test_extra_tokens_relaxes_lexer_errors(self): |
2431 | 2510 | cases = [ |
2432 | 2511 | ( |
@@ -2550,16 +2629,50 @@ def test_degraded_fstring_format_spec(self): |
2550 | 2629 | ) |
2551 | 2630 |
|
2552 | 2631 | def test_escaped_fstring_brace_has_a_position_gap(self): |
2553 | | - tokens = self._get_tokens('f"a{{"', extra_tokens=True) |
2554 | | - self.assertEqual( |
2555 | | - [(tok.type, tok.string, tok.start, tok.end) |
2556 | | - for tok in tokens |
2557 | | - if tok.type in {token.FSTRING_MIDDLE, token.FSTRING_END}], |
2558 | | - [ |
2559 | | - (token.FSTRING_MIDDLE, "a{", (1, 2), (1, 4)), |
2560 | | - (token.FSTRING_END, '"', (1, 5), (1, 6)), |
2561 | | - ], |
2562 | | - ) |
| 2632 | + for source, middle in [('f"a{{"', "a{"), ('f"a}}"', "a}")]: |
| 2633 | + with self.subTest(source=source): |
| 2634 | + tokens = self._get_tokens(source, extra_tokens=True) |
| 2635 | + self.assertEqual( |
| 2636 | + [(tok.type, tok.string, tok.start, tok.end) |
| 2637 | + for tok in tokens |
| 2638 | + if tok.type in {token.FSTRING_MIDDLE, token.FSTRING_END}], |
| 2639 | + [ |
| 2640 | + (token.FSTRING_MIDDLE, middle, (1, 2), (1, 4)), |
| 2641 | + (token.FSTRING_END, '"', (1, 5), (1, 6)), |
| 2642 | + ], |
| 2643 | + ) |
| 2644 | + |
| 2645 | + def test_unclosed_parenthesis_error_position_after_non_ascii(self): |
| 2646 | + for extra_tokens in (False, True): |
| 2647 | + with self.subTest(extra_tokens=extra_tokens): |
| 2648 | + with self.assertRaises(tokenize.TokenError) as caught: |
| 2649 | + self._get_tokens("é = (\n", extra_tokens=extra_tokens) |
| 2650 | + self.assertEqual( |
| 2651 | + caught.exception.args, |
| 2652 | + ("unexpected EOF in multi-line statement", (1, 0)), |
| 2653 | + ) |
| 2654 | + |
| 2655 | + def test_line_continuation_error_uses_logical_line_position(self): |
| 2656 | + cases = [ |
| 2657 | + ("é \\'f\n", (1, 6)), |
| 2658 | + ("x1\\\n==\\_", (2, 9)), |
| 2659 | + ] |
| 2660 | + for extra_tokens in (False, True): |
| 2661 | + for source, position in cases: |
| 2662 | + with self.subTest( |
| 2663 | + source=source, |
| 2664 | + extra_tokens=extra_tokens, |
| 2665 | + ): |
| 2666 | + with self.assertRaises(tokenize.TokenError) as caught: |
| 2667 | + self._get_tokens(source, extra_tokens=extra_tokens) |
| 2668 | + self.assertEqual( |
| 2669 | + caught.exception.args, |
| 2670 | + ( |
| 2671 | + "unexpected character after line continuation " |
| 2672 | + "character", |
| 2673 | + position, |
| 2674 | + ), |
| 2675 | + ) |
2563 | 2676 |
|
2564 | 2677 | def test_tolerant_incompatible_prefix_position_after_non_ascii(self): |
2565 | 2678 | with self.assertRaises(tokenize.TokenError) as caught: |
|
0 commit comments