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