test mixing to have basic assertions for ast nodes def assertastequalself ast1 ast2 ensure the comparisons start at an ast node self assertisinstanceast1 ast ast self assertisinstanceast2 ast ast an ast comparison routine modeled after ast dump but instead of string building it traverses the two trees in lockstep def traversecomparea b missingobject if typea is not typeb self failftypea r is not typeb r if isinstancea ast ast for field in a fields value1 getattra field missing value2 getattrb field missing singletons are equal by definition so further testing can be skipped if value1 is not value2 traversecomparevalue1 value2 elif isinstancea list try for node1 node2 in zipa b stricttrue traversecomparenode1 node2 except valueerror attempt a pretty error ala assertsequenceequal len1 lena len2 lenb if len1 len2 what first diff len1 len2 else what second diff len2 len1 msg fwhat list contains diff additional elements raise self failureexceptionmsg from none elif a b self failfa r b r traversecompareast1 ast2 test mixing to have basic assertions for ast nodes ensure the comparisons start at an ast node an ast comparison routine modeled after ast dump but instead of string building it traverses the two trees in lock step singletons are equal by definition so further testing can be skipped attempt a pretty error ala assertsequenceequal
import ast

class ASTTestMixin:
    

    def assertASTEqual(self, ast1, ast2):
        
        self.assertIsInstance(ast1, ast.AST)
        self.assertIsInstance(ast2, ast.AST)

        
        
        
        def traverse_compare(a, b, missing=object()):
            if type(a) is not type(b):
                self.fail(f"{type(a)!r} is not {type(b)!r}")
            if isinstance(a, ast.AST):
                for field in a._fields:
                    value1 = getattr(a, field, missing)
                    value2 = getattr(b, field, missing)
                    
                    
                    if value1 is not value2:
                        traverse_compare(value1, value2)
            elif isinstance(a, list):
                try:
                    for node1, node2 in zip(a, b, strict=True):
                        traverse_compare(node1, node2)
                except ValueError:
                    
                    len1 = len(a)
                    len2 = len(b)
                    if len1 > len2:
                        what = "First"
                        diff = len1 - len2
                    else:
                        what = "Second"
                        diff = len2 - len1
                    msg = f"{what} list contains {diff} additional elements."
                    raise self.failureException(msg) from None
            elif a != b:
                self.fail(f"{a!r} != {b!r}")
        traverse_compare(ast1, ast2)