@@ -2236,6 +2236,13 @@ def test_number_starting_with_zero(self):
22362236 self .assertEqual (tokens , expected_tokens )
22372237
22382238class CTokenizeTest (TestCase ):
2239+ @staticmethod
2240+ def _get_tokens (source , * , extra_tokens = False ):
2241+ return list (tokenize ._generate_tokens_from_c_tokenizer (
2242+ StringIO (source ).readline ,
2243+ extra_tokens = extra_tokens ,
2244+ ))
2245+
22392246 def check_tokenize (self , s , expected ):
22402247 # Format the tokens in s in a table format.
22412248 # The ENDMARKER and final NEWLINE are omitted.
@@ -2266,6 +2273,159 @@ def readline(encoding):
22662273 ))
22672274 self .assertEqual (tokens , expected )
22682275
2276+ def test_extra_tokens_relaxes_lexer_errors (self ):
2277+ cases = [
2278+ (
2279+ "2sin(x)" ,
2280+ ("invalid decimal literal" , (1 , 1 )),
2281+ [
2282+ (token .NUMBER , "2" , (1 , 0 ), (1 , 1 )),
2283+ (token .NAME , "sin" , (1 , 1 ), (1 , 4 )),
2284+ (token .OP , "(" , (1 , 4 ), (1 , 5 )),
2285+ (token .NAME , "x" , (1 , 5 ), (1 , 6 )),
2286+ (token .OP , ")" , (1 , 6 ), (1 , 7 )),
2287+ ],
2288+ ),
2289+ (
2290+ "01234" ,
2291+ (
2292+ "leading zeros in decimal integer literals are not permitted; "
2293+ "use an 0o prefix for octal integers" ,
2294+ (1 , 1 ),
2295+ ),
2296+ [(token .NUMBER , "01234" , (1 , 0 ), (1 , 5 ))],
2297+ ),
2298+ (
2299+ ")" ,
2300+ ("unmatched ')'" , (1 , 1 )),
2301+ [(token .OP , ")" , (1 , 0 ), (1 , 1 ))],
2302+ ),
2303+ (
2304+ "(]" ,
2305+ (
2306+ "closing parenthesis ']' does not match opening parenthesis '('" ,
2307+ (1 , 2 ),
2308+ ),
2309+ [
2310+ (token .OP , "(" , (1 , 0 ), (1 , 1 )),
2311+ (token .OP , "]" , (1 , 1 ), (1 , 2 )),
2312+ ],
2313+ ),
2314+ (
2315+ "a☃b" ,
2316+ ("invalid character '☃' (U+2603)" , (1 , 2 )),
2317+ [(token .NAME , "a☃b" , (1 , 0 ), (1 , 3 ))],
2318+ ),
2319+ ]
2320+
2321+ for source , error , expected in cases :
2322+ with self .subTest (source = source ):
2323+ with self .assertRaises (tokenize .TokenError ) as caught :
2324+ self ._get_tokens (source )
2325+ self .assertEqual (caught .exception .args , error )
2326+
2327+ tokens = self ._get_tokens (source , extra_tokens = True )
2328+ self .assertEqual (
2329+ [(tok .type , tok .string , tok .start , tok .end )
2330+ for tok in tokens [:- 2 ]],
2331+ expected ,
2332+ )
2333+
2334+ def test_extra_tokens_emits_comments_and_real_indent_locations (self ):
2335+ source = "if x:\n # c\n y\n z\n "
2336+ kinds = {token .COMMENT , token .NL , token .INDENT , token .DEDENT }
2337+
2338+ parser_tokens = self ._get_tokens (source )
2339+ self .assertEqual (
2340+ [(tok .type , tok .string , tok .start , tok .end )
2341+ for tok in parser_tokens if tok .type in kinds ],
2342+ [
2343+ (token .INDENT , "" , (3 , - 1 ), (3 , - 1 )),
2344+ (token .DEDENT , "" , (4 , - 1 ), (4 , - 1 )),
2345+ ],
2346+ )
2347+
2348+ tolerant_tokens = self ._get_tokens (source , extra_tokens = True )
2349+ self .assertEqual (
2350+ [(tok .type , tok .string , tok .start , tok .end )
2351+ for tok in tolerant_tokens if tok .type in kinds ],
2352+ [
2353+ (token .COMMENT , "# c" , (2 , 2 ), (2 , 5 )),
2354+ (token .NL , "\n " , (2 , 5 ), (2 , 6 )),
2355+ (token .INDENT , " " , (3 , 0 ), (3 , 2 )),
2356+ (token .DEDENT , "" , (4 , 0 ), (4 , 0 )),
2357+ ],
2358+ )
2359+
2360+ def test_printable_invalid_operator_streams_in_both_modes (self ):
2361+ for extra_tokens in (False , True ):
2362+ with self .subTest (extra_tokens = extra_tokens ):
2363+ first = self ._get_tokens (
2364+ "1 $ 2" ,
2365+ extra_tokens = extra_tokens ,
2366+ )[1 ]
2367+ self .assertEqual (
2368+ (first .type , first .string , first .start , first .end ),
2369+ (token .OP , "$" , (1 , 2 ), (1 , 3 )),
2370+ )
2371+
2372+ def test_degraded_fstring_format_spec (self ):
2373+ tokens = self ._get_tokens ('f"{1:{2}{{3}}}"' )
2374+ self .assertEqual (
2375+ [(tok .string , tok .start , tok .end )
2376+ for tok in tokens if tok .type == token .FSTRING_MIDDLE ],
2377+ [
2378+ ("{" , (1 , 8 ), (1 , 9 )),
2379+ ("3" , (1 , 10 ), (1 , 11 )),
2380+ ("}" , (1 , 12 ), (1 , 13 )),
2381+ ],
2382+ )
2383+
2384+ tokens = self ._get_tokens ('f"{x:{y}}"' )
2385+ self .assertEqual (
2386+ [(tok .string , tok .start , tok .end )
2387+ for tok in tokens if tok .type == token .FSTRING_MIDDLE ],
2388+ [("" , (1 , 8 ), (1 , 8 ))],
2389+ )
2390+
2391+ with self .assertRaises (tokenize .TokenError ) as caught :
2392+ self ._get_tokens ('f"{1:{2}x}}y"' )
2393+ self .assertEqual (
2394+ caught .exception .args ,
2395+ ("f-string: single '}' is not allowed" , (1 , 11 )),
2396+ )
2397+
2398+ def test_escaped_fstring_brace_has_a_position_gap (self ):
2399+ tokens = self ._get_tokens ('f"a{{"' , extra_tokens = True )
2400+ self .assertEqual (
2401+ [(tok .type , tok .string , tok .start , tok .end )
2402+ for tok in tokens
2403+ if tok .type in {token .FSTRING_MIDDLE , token .FSTRING_END }],
2404+ [
2405+ (token .FSTRING_MIDDLE , "a{" , (1 , 2 ), (1 , 4 )),
2406+ (token .FSTRING_END , '"' , (1 , 5 ), (1 , 6 )),
2407+ ],
2408+ )
2409+
2410+ def test_tolerant_incompatible_prefix_position_after_non_ascii (self ):
2411+ with self .assertRaises (tokenize .TokenError ) as caught :
2412+ self ._get_tokens ('bé )tf"2 ' , extra_tokens = True )
2413+ self .assertEqual (
2414+ caught .exception .args ,
2415+ ("'f' and 't' prefixes are incompatible" , (1 , 6 )),
2416+ )
2417+
2418+ def test_tolerant_fstring_closer_at_expression_entry_depth (self ):
2419+ source = "f'1:{]]{}}r}''"
2420+ for extra_tokens , position in ((False , (1 , 6 )), (True , (1 , 7 ))):
2421+ with self .subTest (extra_tokens = extra_tokens ):
2422+ with self .assertRaises (tokenize .TokenError ) as caught :
2423+ self ._get_tokens (source , extra_tokens = extra_tokens )
2424+ self .assertEqual (
2425+ caught .exception .args ,
2426+ ("f-string: unmatched ']'" , position ),
2427+ )
2428+
22692429 def test_int (self ):
22702430
22712431 self .check_tokenize ('0xff <= 255' , """\
0 commit comments