@@ -176,6 +176,100 @@ def test_string_continuation(self, symbol, prefix):
176176 with self .assertRaises (_IncompleteInputError ):
177177 Compile ()(source , '<input>' , symbol )
178178
179+ @subTests ('symbol' , ('single' , 'exec' , 'eval' ))
180+ @subTests ('prefix' , ('f' , 't' ))
181+ @subTests ('quote' , ('"' , '"""' ))
182+ def test_invalid_escape_in_incomplete_string (self , symbol , prefix , quote ):
183+ for escape in (r'\xZZ' , r'\uZZZZ' , r'\U00110000' , r'\N{NOPE}' ,
184+ r'\x' , r'\u12' ):
185+ for body in (escape , escape + '{0}' , '{0}' + escape ,
186+ '{0:' + escape ):
187+ source = prefix + quote + body + '\\ '
188+ for compiler in COMPILERS :
189+ with self .subTest (source = source , compiler = compiler ):
190+ with self .assertRaisesRegex (SyntaxError , r'\(unicode error\)' ) as cm :
191+ compiler (source , '<input>' , symbol )
192+ self .assertGreater (cm .exception .end_offset , 0 )
193+
194+ @subTests ('symbol' , ('single' , 'exec' , 'eval' ))
195+ @subTests ('prefix' , ('f' , 't' ))
196+ @subTests ('newline' , ('\n ' , '\r ' , '\r \n ' ))
197+ def test_incomplete_escape_locations (self , symbol , prefix , newline ):
198+ cases = (
199+ (r'\N{NOPE}' , (1 , 5 , 1 , 13 )),
200+ ('é\n {0}é' + r'\xZZ' , (2 , 4 , 2 , 9 )),
201+ ('éé\n 中' + r'\xZZ' , (1 , 5 , 2 , 6 )),
202+ ('\n {0:' + r'\xZZ' , (2 , 4 , 2 , 8 )),
203+ )
204+ for body , location in cases :
205+ source = (prefix + '"""' + body ).replace ('\n ' , newline )
206+ for compiler in COMPILERS :
207+ with self .subTest (source = source , compiler = compiler ):
208+ with self .assertRaisesRegex (SyntaxError , r'\(unicode error\)' ) as cm :
209+ compiler (source , '<input>' , symbol )
210+ error = cm .exception
211+ self .assertEqual ((error .lineno , error .offset ,
212+ error .end_lineno , error .end_offset ), location )
213+ self .assertEqual (error .text .rstrip ('\n ' ),
214+ source .splitlines ()[error .lineno - 1 ])
215+
216+ @subTests ('compiler' , WRAPPING_COMPILERS )
217+ def test_non_decode_error_diagnostic (self , compiler ):
218+ source = '+\\ \n '
219+ with self .assertRaises (SyntaxError ) as expected :
220+ compile (source , '<input>' , 'single' )
221+ with self .assertRaises (SyntaxError ) as actual :
222+ compiler (source , '<input>' , 'single' )
223+ self .assertEqual (actual .exception .args , expected .exception .args )
224+
225+ @subTests ('compiler' , COMPILERS )
226+ @subTests ('prefix' , ('f' , 't' ))
227+ def test_incomplete_escape_warning (self , compiler , prefix ):
228+ source = r"'\e'; " + prefix + '"""' + r'\N{NOPE}'
229+ with warnings .catch_warnings (record = True , action = 'always' ) as caught :
230+ with self .assertRaisesRegex (SyntaxError , 'unknown Unicode character name' ):
231+ compiler (source , '<input>' , 'single' )
232+ self .assertEqual ([warning .category for warning in caught ], [SyntaxWarning ])
233+ with warnings .catch_warnings (action = 'error' , category = SyntaxWarning ):
234+ with self .assertRaisesRegex (SyntaxError , 'invalid escape sequence' ):
235+ compiler (source , '<input>' , 'single' )
236+
237+ @subTests ('symbol' , ('single' , 'exec' , 'eval' ))
238+ @subTests ('prefix' , ('f' , 't' ))
239+ @subTests ('quote' , ('"' , '"""' ))
240+ def test_valid_escape_in_incomplete_string (self , symbol , prefix , quote ):
241+ for escape in (r'\x41' , r'\u00e9' , r'\U0001f600' , r'\\xZZ' ):
242+ source = prefix + quote + escape + '\\ '
243+ with self .subTest (escape = escape ):
244+ self .assertIsNone (compile_command (source , symbol = symbol ))
245+ with self .assertRaises (_IncompleteInputError ):
246+ Compile ()(source , '<input>' , symbol )
247+ for compiler in COMPILERS :
248+ self .assertValid (source + '\n ' + quote , symbol ,
249+ compiler = compiler )
250+
251+ source = 'r' + prefix + quote + r'\xZZ' + '\\ '
252+ self .assertIsNone (compile_command (source , symbol = symbol ))
253+ with self .assertRaises (_IncompleteInputError ):
254+ Compile ()(source , '<input>' , symbol )
255+ for compiler in COMPILERS :
256+ self .assertValid (source + '\n ' + quote , symbol , compiler = compiler )
257+
258+ @subTests ('symbol' , ('single' , 'exec' , 'eval' ))
259+ @subTests ('prefix' , ('f' , 't' ))
260+ def test_unfinished_escape_in_incomplete_string (self , symbol , prefix ):
261+ for escape , suffix in ((r'\x' , '41' ), (r'\u12' , '34' ),
262+ (r'\U0001' , 'f600' ),
263+ (r'\N{LATIN' , ' SMALL LETTER A}' )):
264+ source = prefix + '"""' + escape
265+ with self .subTest (escape = escape ):
266+ self .assertIsNone (compile_command (source , symbol = symbol ))
267+ with self .assertRaises (_IncompleteInputError ):
268+ Compile ()(source , '<input>' , symbol )
269+ for compiler in COMPILERS :
270+ self .assertValid (source + suffix + '"""' , symbol ,
271+ compiler = compiler )
272+
179273 @subTests ('symbol' , ('single' , 'exec' , 'eval' ))
180274 @subTests ('newline' , ('\n ' , '\r ' , '\r \n ' ))
181275 def test_continued_final_line (self , symbol , newline ):
0 commit comments