1"""Test suite for 2to3's parser and grammar files. 2 3This is the place to add tests for changes to 2to3's grammar, such as those 4merging the grammars for Python 2 and 3. In addition to specific tests for 5parts of the grammar we've changed, we also make sure we can parse the 6test_grammar.py files from both Python 2 and Python 3. 7""" 8 9# Testing imports 10from . import support 11from .support import driver, driver_no_print_statement 12 13# Python imports 14import difflib 15import importlib 16import operator 17import os 18import pickle 19import shutil 20import subprocess 21import sys 22import tempfile 23import test.support 24import unittest 25 26# Local imports 27from lib2to3.pgen2 import driver as pgen2_driver 28from lib2to3.pgen2 import tokenize 29from ..pgen2.parse import ParseError 30from lib2to3.pygram import python_symbols as syms 31 32 33class TestDriver(support.TestCase): 34 35 def test_formfeed(self): 36 s = """print 1\n\x0Cprint 2\n""" 37 t = driver.parse_string(s) 38 self.assertEqual(t.children[0].children[0].type, syms.print_stmt) 39 self.assertEqual(t.children[1].children[0].type, syms.print_stmt) 40 41 42class TestPgen2Caching(support.TestCase): 43 def test_load_grammar_from_txt_file(self): 44 pgen2_driver.load_grammar(support.grammar_path, save=False, force=True) 45 46 def test_load_grammar_from_pickle(self): 47 # Make a copy of the grammar file in a temp directory we are 48 # guaranteed to be able to write to. 49 tmpdir = tempfile.mkdtemp() 50 try: 51 grammar_copy = os.path.join( 52 tmpdir, os.path.basename(support.grammar_path)) 53 shutil.copy(support.grammar_path, grammar_copy) 54 pickle_name = pgen2_driver._generate_pickle_name(grammar_copy) 55 56 pgen2_driver.load_grammar(grammar_copy, save=True, force=True) 57 self.assertTrue(os.path.exists(pickle_name)) 58 59 os.unlink(grammar_copy) # Only the pickle remains... 60 pgen2_driver.load_grammar(grammar_copy, save=False, force=False) 61 finally: 62 shutil.rmtree(tmpdir) 63 64 @unittest.skipIf(sys.executable is None, 'sys.executable required') 65 @unittest.skipIf( 66 sys.platform in {'emscripten', 'wasi'}, 'requires working subprocess' 67 ) 68 def test_load_grammar_from_subprocess(self): 69 tmpdir = tempfile.mkdtemp() 70 tmpsubdir = os.path.join(tmpdir, 'subdir') 71 try: 72 os.mkdir(tmpsubdir) 73 grammar_base = os.path.basename(support.grammar_path) 74 grammar_copy = os.path.join(tmpdir, grammar_base) 75 grammar_sub_copy = os.path.join(tmpsubdir, grammar_base) 76 shutil.copy(support.grammar_path, grammar_copy) 77 shutil.copy(support.grammar_path, grammar_sub_copy) 78 pickle_name = pgen2_driver._generate_pickle_name(grammar_copy) 79 pickle_sub_name = pgen2_driver._generate_pickle_name( 80 grammar_sub_copy) 81 self.assertNotEqual(pickle_name, pickle_sub_name) 82 83 # Generate a pickle file from this process. 84 pgen2_driver.load_grammar(grammar_copy, save=True, force=True) 85 self.assertTrue(os.path.exists(pickle_name)) 86 87 # Generate a new pickle file in a subprocess with a most likely 88 # different hash randomization seed. 89 sub_env = dict(os.environ) 90 sub_env['PYTHONHASHSEED'] = 'random' 91 code = """ 92from lib2to3.pgen2 import driver as pgen2_driver 93pgen2_driver.load_grammar(%r, save=True, force=True) 94 """ % (grammar_sub_copy,) 95 cmd = [sys.executable, 96 '-Wignore:lib2to3:DeprecationWarning', 97 '-c', code] 98 subprocess.check_call( cmd, env=sub_env) 99 self.assertTrue(os.path.exists(pickle_sub_name)) 100 101 with open(pickle_name, 'rb') as pickle_f_1, \ 102 open(pickle_sub_name, 'rb') as pickle_f_2: 103 self.assertEqual( 104 pickle_f_1.read(), pickle_f_2.read(), 105 msg='Grammar caches generated using different hash seeds' 106 ' were not identical.') 107 finally: 108 shutil.rmtree(tmpdir) 109 110 def test_load_packaged_grammar(self): 111 modname = __name__ + '.load_test' 112 class MyLoader: 113 def get_data(self, where): 114 return pickle.dumps({'elephant': 19}) 115 class MyModule: 116 __file__ = 'parsertestmodule' 117 __spec__ = importlib.util.spec_from_loader(modname, MyLoader()) 118 sys.modules[modname] = MyModule() 119 self.addCleanup(operator.delitem, sys.modules, modname) 120 g = pgen2_driver.load_packaged_grammar(modname, 'Grammar.txt') 121 self.assertEqual(g.elephant, 19) 122 123 124class GrammarTest(support.TestCase): 125 def validate(self, code): 126 support.parse_string(code) 127 128 def invalid_syntax(self, code): 129 try: 130 self.validate(code) 131 except ParseError: 132 pass 133 else: 134 raise AssertionError("Syntax shouldn't have been valid") 135 136 137class TestMatrixMultiplication(GrammarTest): 138 def test_matrix_multiplication_operator(self): 139 self.validate("a @ b") 140 self.validate("a @= b") 141 142 143class TestYieldFrom(GrammarTest): 144 def test_yield_from(self): 145 self.validate("yield from x") 146 self.validate("(yield from x) + y") 147 self.invalid_syntax("yield from") 148 149 150class TestAsyncAwait(GrammarTest): 151 def test_await_expr(self): 152 self.validate("""async def foo(): 153 await x 154 """) 155 156 self.validate("""async def foo(): 157 [i async for i in b] 158 """) 159 160 self.validate("""async def foo(): 161 {i for i in b 162 async for i in a if await i 163 for b in i} 164 """) 165 166 self.validate("""async def foo(): 167 [await i for i in b if await c] 168 """) 169 170 self.validate("""async def foo(): 171 [ i for i in b if c] 172 """) 173 174 self.validate("""async def foo(): 175 176 def foo(): pass 177 178 def foo(): pass 179 180 await x 181 """) 182 183 self.validate("""async def foo(): return await a""") 184 185 self.validate("""def foo(): 186 def foo(): pass 187 async def foo(): await x 188 """) 189 190 self.invalid_syntax("await x") 191 self.invalid_syntax("""def foo(): 192 await x""") 193 194 self.invalid_syntax("""def foo(): 195 def foo(): pass 196 async def foo(): pass 197 await x 198 """) 199 200 def test_async_var(self): 201 self.validate("""async = 1""") 202 self.validate("""await = 1""") 203 self.validate("""def async(): pass""") 204 205 def test_async_for(self): 206 self.validate("""async def foo(): 207 async for a in b: pass""") 208 209 def test_async_with(self): 210 self.validate("""async def foo(): 211 async with a: pass""") 212 213 self.invalid_syntax("""def foo(): 214 async with a: pass""") 215 216 def test_async_generator(self): 217 self.validate( 218 """async def foo(): 219 return (i * 2 async for i in arange(42))""" 220 ) 221 self.validate( 222 """def foo(): 223 return (i * 2 async for i in arange(42))""" 224 ) 225 226 227class TestRaiseChanges(GrammarTest): 228 def test_2x_style_1(self): 229 self.validate("raise") 230 231 def test_2x_style_2(self): 232 self.validate("raise E, V") 233 234 def test_2x_style_3(self): 235 self.validate("raise E, V, T") 236 237 def test_2x_style_invalid_1(self): 238 self.invalid_syntax("raise E, V, T, Z") 239 240 def test_3x_style(self): 241 self.validate("raise E1 from E2") 242 243 def test_3x_style_invalid_1(self): 244 self.invalid_syntax("raise E, V from E1") 245 246 def test_3x_style_invalid_2(self): 247 self.invalid_syntax("raise E from E1, E2") 248 249 def test_3x_style_invalid_3(self): 250 self.invalid_syntax("raise from E1, E2") 251 252 def test_3x_style_invalid_4(self): 253 self.invalid_syntax("raise E from") 254 255 256# Modelled after Lib/test/test_grammar.py:TokenTests.test_funcdef issue2292 257# and Lib/test/text_parser.py test_list_displays, test_set_displays, 258# test_dict_displays, test_argument_unpacking, ... changes. 259class TestUnpackingGeneralizations(GrammarTest): 260 def test_mid_positional_star(self): 261 self.validate("""func(1, *(2, 3), 4)""") 262 263 def test_double_star_dict_literal(self): 264 self.validate("""func(**{'eggs':'scrambled', 'spam':'fried'})""") 265 266 def test_double_star_dict_literal_after_keywords(self): 267 self.validate("""func(spam='fried', **{'eggs':'scrambled'})""") 268 269 def test_double_star_expression(self): 270 self.validate("""func(**{'a':2} or {})""") 271 self.validate("""func(**() or {})""") 272 273 def test_star_expression(self): 274 self.validate("""func(*[] or [2])""") 275 276 def test_list_display(self): 277 self.validate("""[*{2}, 3, *[4]]""") 278 279 def test_set_display(self): 280 self.validate("""{*{2}, 3, *[4]}""") 281 282 def test_dict_display_1(self): 283 self.validate("""{**{}}""") 284 285 def test_dict_display_2(self): 286 self.validate("""{**{}, 3:4, **{5:6, 7:8}}""") 287 288 def test_complex_star_expression(self): 289 self.validate("func(* [] or [1])") 290 291 def test_complex_double_star_expression(self): 292 self.validate("func(**{1: 3} if False else {x: x for x in range(3)})") 293 294 def test_argument_unpacking_1(self): 295 self.validate("""f(a, *b, *c, d)""") 296 297 def test_argument_unpacking_2(self): 298 self.validate("""f(**a, **b)""") 299 300 def test_argument_unpacking_3(self): 301 self.validate("""f(2, *a, *b, **b, **c, **d)""") 302 303 def test_trailing_commas_1(self): 304 self.validate("def f(a, b): call(a, b)") 305 self.validate("def f(a, b,): call(a, b,)") 306 307 def test_trailing_commas_2(self): 308 self.validate("def f(a, *b): call(a, *b)") 309 self.validate("def f(a, *b,): call(a, *b,)") 310 311 def test_trailing_commas_3(self): 312 self.validate("def f(a, b=1): call(a, b=1)") 313 self.validate("def f(a, b=1,): call(a, b=1,)") 314 315 def test_trailing_commas_4(self): 316 self.validate("def f(a, **b): call(a, **b)") 317 self.validate("def f(a, **b,): call(a, **b,)") 318 319 def test_trailing_commas_5(self): 320 self.validate("def f(*a, b=1): call(*a, b=1)") 321 self.validate("def f(*a, b=1,): call(*a, b=1,)") 322 323 def test_trailing_commas_6(self): 324 self.validate("def f(*a, **b): call(*a, **b)") 325 self.validate("def f(*a, **b,): call(*a, **b,)") 326 327 def test_trailing_commas_7(self): 328 self.validate("def f(*, b=1): call(*b)") 329 self.validate("def f(*, b=1,): call(*b,)") 330 331 def test_trailing_commas_8(self): 332 self.validate("def f(a=1, b=2): call(a=1, b=2)") 333 self.validate("def f(a=1, b=2,): call(a=1, b=2,)") 334 335 def test_trailing_commas_9(self): 336 self.validate("def f(a=1, **b): call(a=1, **b)") 337 self.validate("def f(a=1, **b,): call(a=1, **b,)") 338 339 def test_trailing_commas_lambda_1(self): 340 self.validate("f = lambda a, b: call(a, b)") 341 self.validate("f = lambda a, b,: call(a, b,)") 342 343 def test_trailing_commas_lambda_2(self): 344 self.validate("f = lambda a, *b: call(a, *b)") 345 self.validate("f = lambda a, *b,: call(a, *b,)") 346 347 def test_trailing_commas_lambda_3(self): 348 self.validate("f = lambda a, b=1: call(a, b=1)") 349 self.validate("f = lambda a, b=1,: call(a, b=1,)") 350 351 def test_trailing_commas_lambda_4(self): 352 self.validate("f = lambda a, **b: call(a, **b)") 353 self.validate("f = lambda a, **b,: call(a, **b,)") 354 355 def test_trailing_commas_lambda_5(self): 356 self.validate("f = lambda *a, b=1: call(*a, b=1)") 357 self.validate("f = lambda *a, b=1,: call(*a, b=1,)") 358 359 def test_trailing_commas_lambda_6(self): 360 self.validate("f = lambda *a, **b: call(*a, **b)") 361 self.validate("f = lambda *a, **b,: call(*a, **b,)") 362 363 def test_trailing_commas_lambda_7(self): 364 self.validate("f = lambda *, b=1: call(*b)") 365 self.validate("f = lambda *, b=1,: call(*b,)") 366 367 def test_trailing_commas_lambda_8(self): 368 self.validate("f = lambda a=1, b=2: call(a=1, b=2)") 369 self.validate("f = lambda a=1, b=2,: call(a=1, b=2,)") 370 371 def test_trailing_commas_lambda_9(self): 372 self.validate("f = lambda a=1, **b: call(a=1, **b)") 373 self.validate("f = lambda a=1, **b,: call(a=1, **b,)") 374 375 376# Adapted from Python 3's Lib/test/test_grammar.py:GrammarTests.testFuncdef 377class TestFunctionAnnotations(GrammarTest): 378 def test_1(self): 379 self.validate("""def f(x) -> list: pass""") 380 381 def test_2(self): 382 self.validate("""def f(x:int): pass""") 383 384 def test_3(self): 385 self.validate("""def f(*x:str): pass""") 386 387 def test_4(self): 388 self.validate("""def f(**x:float): pass""") 389 390 def test_5(self): 391 self.validate("""def f(x, y:1+2): pass""") 392 393 def test_6(self): 394 self.validate("""def f(a, (b:1, c:2, d)): pass""") 395 396 def test_7(self): 397 self.validate("""def f(a, (b:1, c:2, d), e:3=4, f=5, *g:6): pass""") 398 399 def test_8(self): 400 s = """def f(a, (b:1, c:2, d), e:3=4, f=5, 401 *g:6, h:7, i=8, j:9=10, **k:11) -> 12: pass""" 402 self.validate(s) 403 404 def test_9(self): 405 s = """def f( 406 a: str, 407 b: int, 408 *, 409 c: bool = False, 410 **kwargs, 411 ) -> None: 412 call(c=c, **kwargs,)""" 413 self.validate(s) 414 415 def test_10(self): 416 s = """def f( 417 a: str, 418 ) -> None: 419 call(a,)""" 420 self.validate(s) 421 422 def test_11(self): 423 s = """def f( 424 a: str = '', 425 ) -> None: 426 call(a=a,)""" 427 self.validate(s) 428 429 def test_12(self): 430 s = """def f( 431 *args: str, 432 ) -> None: 433 call(*args,)""" 434 self.validate(s) 435 436 def test_13(self): 437 self.validate("def f(a: str, b: int) -> None: call(a, b)") 438 self.validate("def f(a: str, b: int,) -> None: call(a, b,)") 439 440 def test_14(self): 441 self.validate("def f(a: str, *b: int) -> None: call(a, *b)") 442 self.validate("def f(a: str, *b: int,) -> None: call(a, *b,)") 443 444 def test_15(self): 445 self.validate("def f(a: str, b: int=1) -> None: call(a, b=1)") 446 self.validate("def f(a: str, b: int=1,) -> None: call(a, b=1,)") 447 448 def test_16(self): 449 self.validate("def f(a: str, **b: int) -> None: call(a, **b)") 450 self.validate("def f(a: str, **b: int,) -> None: call(a, **b,)") 451 452 def test_17(self): 453 self.validate("def f(*a: str, b: int=1) -> None: call(*a, b=1)") 454 self.validate("def f(*a: str, b: int=1,) -> None: call(*a, b=1,)") 455 456 def test_18(self): 457 self.validate("def f(*a: str, **b: int) -> None: call(*a, **b)") 458 self.validate("def f(*a: str, **b: int,) -> None: call(*a, **b,)") 459 460 def test_19(self): 461 self.validate("def f(*, b: int=1) -> None: call(*b)") 462 self.validate("def f(*, b: int=1,) -> None: call(*b,)") 463 464 def test_20(self): 465 self.validate("def f(a: str='', b: int=2) -> None: call(a=a, b=2)") 466 self.validate("def f(a: str='', b: int=2,) -> None: call(a=a, b=2,)") 467 468 def test_21(self): 469 self.validate("def f(a: str='', **b: int) -> None: call(a=a, **b)") 470 self.validate("def f(a: str='', **b: int,) -> None: call(a=a, **b,)") 471 472 473# Adapted from Python 3's Lib/test/test_grammar.py:GrammarTests.test_var_annot 474class TestVarAnnotations(GrammarTest): 475 def test_1(self): 476 self.validate("var1: int = 5") 477 478 def test_2(self): 479 self.validate("var2: [int, str]") 480 481 def test_3(self): 482 self.validate("def f():\n" 483 " st: str = 'Hello'\n" 484 " a.b: int = (1, 2)\n" 485 " return st\n") 486 487 def test_4(self): 488 self.validate("def fbad():\n" 489 " x: int\n" 490 " print(x)\n") 491 492 def test_5(self): 493 self.validate("class C:\n" 494 " x: int\n" 495 " s: str = 'attr'\n" 496 " z = 2\n" 497 " def __init__(self, x):\n" 498 " self.x: int = x\n") 499 500 def test_6(self): 501 self.validate("lst: List[int] = []") 502 503 504class TestExcept(GrammarTest): 505 def test_new(self): 506 s = """ 507 try: 508 x 509 except E as N: 510 y""" 511 self.validate(s) 512 513 def test_old(self): 514 s = """ 515 try: 516 x 517 except E, N: 518 y""" 519 self.validate(s) 520 521 522class TestStringLiterals(GrammarTest): 523 prefixes = ("'", '"', 524 "r'", 'r"', "R'", 'R"', 525 "u'", 'u"', "U'", 'U"', 526 "b'", 'b"', "B'", 'B"', 527 "f'", 'f"', "F'", 'F"', 528 "ur'", 'ur"', "Ur'", 'Ur"', 529 "uR'", 'uR"', "UR'", 'UR"', 530 "br'", 'br"', "Br'", 'Br"', 531 "bR'", 'bR"', "BR'", 'BR"', 532 "rb'", 'rb"', "Rb'", 'Rb"', 533 "rB'", 'rB"', "RB'", 'RB"',) 534 535 def test_lit(self): 536 for pre in self.prefixes: 537 single = "{p}spamspamspam{s}".format(p=pre, s=pre[-1]) 538 self.validate(single) 539 triple = "{p}{s}{s}eggs{s}{s}{s}".format(p=pre, s=pre[-1]) 540 self.validate(triple) 541 542 543# Adapted from Python 3's Lib/test/test_grammar.py:GrammarTests.testAtoms 544class TestSetLiteral(GrammarTest): 545 def test_1(self): 546 self.validate("""x = {'one'}""") 547 548 def test_2(self): 549 self.validate("""x = {'one', 1,}""") 550 551 def test_3(self): 552 self.validate("""x = {'one', 'two', 'three'}""") 553 554 def test_4(self): 555 self.validate("""x = {2, 3, 4,}""") 556 557 558# Adapted from Python 3's Lib/test/test_unicode_identifiers.py and 559# Lib/test/test_tokenize.py:TokenizeTest.test_non_ascii_identifiers 560class TestIdentifier(GrammarTest): 561 def test_non_ascii_identifiers(self): 562 self.validate("Örter = 'places'\ngrün = 'green'") 563 self.validate("蟒 = a蟒 = 锦蛇 = 1") 564 self.validate("µ = aµ = µµ = 1") 565 self.validate(" = a_ = 1") 566 567 568class TestNumericLiterals(GrammarTest): 569 def test_new_octal_notation(self): 570 self.validate("""0o7777777777777""") 571 self.invalid_syntax("""0o7324528887""") 572 573 def test_new_binary_notation(self): 574 self.validate("""0b101010""") 575 self.invalid_syntax("""0b0101021""") 576 577 578class TestClassDef(GrammarTest): 579 def test_new_syntax(self): 580 self.validate("class B(t=7): pass") 581 self.validate("class B(t, *args): pass") 582 self.validate("class B(t, **kwargs): pass") 583 self.validate("class B(t, *args, **kwargs): pass") 584 self.validate("class B(t, y=9, *args, **kwargs,): pass") 585 586 587class TestParserIdempotency(support.TestCase): 588 589 """A cut-down version of pytree_idempotency.py.""" 590 591 def parse_file(self, filepath): 592 if test.support.verbose: 593 print(f"Parse file: {filepath}") 594 with open(filepath, "rb") as fp: 595 encoding = tokenize.detect_encoding(fp.readline)[0] 596 self.assertIsNotNone(encoding, 597 "can't detect encoding for %s" % filepath) 598 with open(filepath, "r", encoding=encoding) as fp: 599 source = fp.read() 600 try: 601 tree = driver.parse_string(source) 602 except ParseError: 603 try: 604 tree = driver_no_print_statement.parse_string(source) 605 except ParseError as err: 606 self.fail('ParseError on file %s (%s)' % (filepath, err)) 607 new = str(tree) 608 if new != source: 609 print(diff_texts(source, new, filepath)) 610 self.fail("Idempotency failed: %s" % filepath) 611 612 def test_all_project_files(self): 613 for filepath in support.all_project_files(): 614 with self.subTest(filepath=filepath): 615 self.parse_file(filepath) 616 617 def test_extended_unpacking(self): 618 driver.parse_string("a, *b, c = x\n") 619 driver.parse_string("[*a, b] = x\n") 620 driver.parse_string("(z, *y, w) = m\n") 621 driver.parse_string("for *z, m in d: pass\n") 622 623 624class TestLiterals(GrammarTest): 625 626 def validate(self, s): 627 driver.parse_string(support.dedent(s) + "\n\n") 628 629 def test_multiline_bytes_literals(self): 630 s = """ 631 md5test(b"\xaa" * 80, 632 (b"Test Using Larger Than Block-Size Key " 633 b"and Larger Than One Block-Size Data"), 634 "6f630fad67cda0ee1fb1f562db3aa53e") 635 """ 636 self.validate(s) 637 638 def test_multiline_bytes_tripquote_literals(self): 639 s = ''' 640 b""" 641 <?xml version="1.0" encoding="UTF-8"?> 642 <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN"> 643 """ 644 ''' 645 self.validate(s) 646 647 def test_multiline_str_literals(self): 648 s = """ 649 md5test("\xaa" * 80, 650 ("Test Using Larger Than Block-Size Key " 651 "and Larger Than One Block-Size Data"), 652 "6f630fad67cda0ee1fb1f562db3aa53e") 653 """ 654 self.validate(s) 655 656 657class TestNamedAssignments(GrammarTest): 658 """Also known as the walrus operator.""" 659 660 def test_named_assignment_if(self): 661 driver.parse_string("if f := x(): pass\n") 662 663 def test_named_assignment_while(self): 664 driver.parse_string("while f := x(): pass\n") 665 666 def test_named_assignment_generator(self): 667 driver.parse_string("any((lastNum := num) == 1 for num in [1, 2, 3])\n") 668 669 def test_named_assignment_listcomp(self): 670 driver.parse_string("[(lastNum := num) == 1 for num in [1, 2, 3]]\n") 671 672 673class TestPositionalOnlyArgs(GrammarTest): 674 675 def test_one_pos_only_arg(self): 676 driver.parse_string("def one_pos_only_arg(a, /): pass\n") 677 678 def test_all_markers(self): 679 driver.parse_string( 680 "def all_markers(a, b=2, /, c, d=4, *, e=5, f): pass\n") 681 682 def test_all_with_args_and_kwargs(self): 683 driver.parse_string( 684 """def all_markers_with_args_and_kwargs( 685 aa, b, /, _cc, d, *args, e, f_f, **kwargs, 686 ): 687 pass\n""") 688 689 def test_lambda_soup(self): 690 driver.parse_string( 691 "lambda a, b, /, c, d, *args, e, f, **kw: kw\n") 692 693 def test_only_positional_or_keyword(self): 694 driver.parse_string("def func(a,b,/,*,g,e=3): pass\n") 695 696 697class TestPickleableException(unittest.TestCase): 698 def test_ParseError(self): 699 err = ParseError('msg', 2, None, (1, 'context')) 700 for proto in range(pickle.HIGHEST_PROTOCOL + 1): 701 err2 = pickle.loads(pickle.dumps(err, protocol=proto)) 702 self.assertEqual(err.args, err2.args) 703 self.assertEqual(err.msg, err2.msg) 704 self.assertEqual(err.type, err2.type) 705 self.assertEqual(err.value, err2.value) 706 self.assertEqual(err.context, err2.context) 707 708 709def diff_texts(a, b, filename): 710 a = a.splitlines() 711 b = b.splitlines() 712 return difflib.unified_diff(a, b, filename, filename, 713 "(original)", "(reserialized)", 714 lineterm="") 715 716 717if __name__ == '__main__': 718 unittest.main() 719