|
7 | 7 | import token |
8 | 8 | import tokenize |
9 | 9 | import unittest |
| 10 | +import warnings |
| 11 | +import weakref |
| 12 | +import _tokenize |
10 | 13 | from io import BytesIO, StringIO |
11 | 14 | from textwrap import dedent |
12 | 15 | from unittest import TestCase, mock |
@@ -2243,6 +2246,38 @@ def _get_tokens(source, *, extra_tokens=False): |
2243 | 2246 | extra_tokens=extra_tokens, |
2244 | 2247 | )) |
2245 | 2248 |
|
| 2249 | + def test_readline_reentry_is_rejected(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(RuntimeError, "already executing"): |
| 2258 | + next(iterator) |
| 2259 | + return weakref.ref(readline) |
| 2260 | + |
| 2261 | + readline_ref = make_cycle() |
| 2262 | + support.gc_collect() |
| 2263 | + self.assertIsNone(readline_ref()) |
| 2264 | + |
| 2265 | + def test_warning_reentry_is_rejected(self): |
| 2266 | + iterator = None |
| 2267 | + |
| 2268 | + def showwarning(*args, **kwargs): |
| 2269 | + next(iterator) |
| 2270 | + |
| 2271 | + with warnings.catch_warnings(): |
| 2272 | + warnings.simplefilter("always") |
| 2273 | + with mock.patch.object(warnings, "showwarning", showwarning): |
| 2274 | + iterator = _tokenize.TokenizerIter( |
| 2275 | + StringIO("1if\n").readline, |
| 2276 | + extra_tokens=False, |
| 2277 | + ) |
| 2278 | + with self.assertRaisesRegex(RuntimeError, "already executing"): |
| 2279 | + next(iterator) |
| 2280 | + |
2246 | 2281 | def check_tokenize(self, s, expected): |
2247 | 2282 | # Format the tokens in s in a table format. |
2248 | 2283 | # The ENDMARKER and final NEWLINE are omitted. |
@@ -2273,6 +2308,71 @@ def readline(encoding): |
2273 | 2308 | )) |
2274 | 2309 | self.assertEqual(tokens, expected) |
2275 | 2310 |
|
| 2311 | + def test_stateful_encoding_across_readline_calls(self): |
| 2312 | + encoded_name = "変数".encode("iso2022_jp") |
| 2313 | + payload = encoded_name[3:-3] |
| 2314 | + lines = iter([ |
| 2315 | + b"# \x1b$B" + payload + b"\n", |
| 2316 | + payload + b"\x1b(B\n", |
| 2317 | + b"", |
| 2318 | + ]) |
| 2319 | + tokens = list(tokenize._generate_tokens_from_c_tokenizer( |
| 2320 | + lines.__next__, |
| 2321 | + extra_tokens=True, |
| 2322 | + encoding="iso2022_jp", |
| 2323 | + )) |
| 2324 | + self.assertEqual( |
| 2325 | + [(tok.type, tok.string) for tok in tokens], |
| 2326 | + [ |
| 2327 | + (token.COMMENT, "# 変数"), |
| 2328 | + (token.NL, "\n"), |
| 2329 | + (token.NAME, "変数"), |
| 2330 | + (token.NEWLINE, "\n"), |
| 2331 | + (token.ENDMARKER, ""), |
| 2332 | + ], |
| 2333 | + ) |
| 2334 | + |
| 2335 | + def test_utf8_decoder_spans_readline_calls(self): |
| 2336 | + lines = iter([b"\xc3", b"\xa9\n", b""]) |
| 2337 | + tokens = list(tokenize._generate_tokens_from_c_tokenizer( |
| 2338 | + lines.__next__, |
| 2339 | + extra_tokens=True, |
| 2340 | + encoding="utf-8", |
| 2341 | + )) |
| 2342 | + self.assertEqual( |
| 2343 | + [(tok.type, tok.string, tok.start, tok.end) for tok in tokens], |
| 2344 | + [ |
| 2345 | + (token.NAME, "é", (1, 0), (1, 1)), |
| 2346 | + (token.NEWLINE, "\n", (1, 1), (1, 2)), |
| 2347 | + (token.ENDMARKER, "", (2, 0), (2, 0)), |
| 2348 | + ], |
| 2349 | + ) |
| 2350 | + |
| 2351 | + def test_encoded_readline_replaces_invalid_bytes(self): |
| 2352 | + lines = iter([b"\xff\n", b""]) |
| 2353 | + tokens = list(tokenize._generate_tokens_from_c_tokenizer( |
| 2354 | + lines.__next__, |
| 2355 | + extra_tokens=True, |
| 2356 | + encoding="utf-8", |
| 2357 | + )) |
| 2358 | + self.assertEqual( |
| 2359 | + [(tok.type, tok.string) for tok in tokens], |
| 2360 | + [ |
| 2361 | + (token.NAME, "�"), |
| 2362 | + (token.NEWLINE, "\n"), |
| 2363 | + (token.ENDMARKER, ""), |
| 2364 | + ], |
| 2365 | + ) |
| 2366 | + |
| 2367 | + def test_encoded_readline_codec_lookup_is_lazy(self): |
| 2368 | + iterator = _tokenize.TokenizerIter( |
| 2369 | + lambda: b"", |
| 2370 | + extra_tokens=True, |
| 2371 | + encoding="missing-tokenizer-codec", |
| 2372 | + ) |
| 2373 | + with self.assertRaises(LookupError): |
| 2374 | + next(iterator) |
| 2375 | + |
2276 | 2376 | def test_extra_tokens_relaxes_lexer_errors(self): |
2277 | 2377 | cases = [ |
2278 | 2378 | ( |
@@ -2407,6 +2507,38 @@ def test_escaped_fstring_brace_has_a_position_gap(self): |
2407 | 2507 | ], |
2408 | 2508 | ) |
2409 | 2509 |
|
| 2510 | + def test_unclosed_parenthesis_error_uses_buffer_relative_position(self): |
| 2511 | + for extra_tokens in (False, True): |
| 2512 | + with self.subTest(extra_tokens=extra_tokens): |
| 2513 | + with self.assertRaises(tokenize.TokenError) as caught: |
| 2514 | + self._get_tokens("é = (\n", extra_tokens=extra_tokens) |
| 2515 | + self.assertEqual( |
| 2516 | + caught.exception.args, |
| 2517 | + ("unexpected EOF in multi-line statement", (1, 0)), |
| 2518 | + ) |
| 2519 | + |
| 2520 | + def test_line_continuation_error_uses_logical_line_position(self): |
| 2521 | + cases = [ |
| 2522 | + ("é \\'f\n", (1, 6)), |
| 2523 | + ("x1\\\n==\\_", (2, 9)), |
| 2524 | + ] |
| 2525 | + for extra_tokens in (False, True): |
| 2526 | + for source, position in cases: |
| 2527 | + with self.subTest( |
| 2528 | + source=source, |
| 2529 | + extra_tokens=extra_tokens, |
| 2530 | + ): |
| 2531 | + with self.assertRaises(tokenize.TokenError) as caught: |
| 2532 | + self._get_tokens(source, extra_tokens=extra_tokens) |
| 2533 | + self.assertEqual( |
| 2534 | + caught.exception.args, |
| 2535 | + ( |
| 2536 | + "unexpected character after line continuation " |
| 2537 | + "character", |
| 2538 | + position, |
| 2539 | + ), |
| 2540 | + ) |
| 2541 | + |
2410 | 2542 | def test_tolerant_incompatible_prefix_position_after_non_ascii(self): |
2411 | 2543 | with self.assertRaises(tokenize.TokenError) as caught: |
2412 | 2544 | self._get_tokens('bé )tf"2 ', extra_tokens=True) |
|
0 commit comments