#$Id: assignment_order.py,v 1.9 2004/03/18 18:07:01 sdobrev Exp $
#s.dobrev 2k4
"""
 Obtain order of assigments in python source text namespaces.
 python loses them at end of exec, as all namespaces
 are/must be plain dicts (set via PyDict_SetItem - there is only
 a temporary array of local vars in exec' frame in C).
 Handles assignment, class def, function def, import.
 Warning: conditional definitions etc runtime stuff is ignored -
          this goes through _all_ the source.
use as:
    a = ASTVisitor4assign()
    klastree = a.parse( src_string1 )
    ###now use klastree (or a.klas which is same)
    klastree.pprint()

    a.parse( src_string2 )    #as if src = src_string1 + src_string2
    a.klas.pprint()
    ###for clean reuse, either do a.__init__() or make another ASTVisitor4assign

    ###for whole module:
    import xxx      #assume there's class X1 inside
    klastree = ASTVisitor4assign().parseModuleSource( xxx )
    klastree.set_order_in_namespace( xxx)   #,flatten=True to pre-calculate flattened order
    print xxx.__order
    print xxx.X1.__order

    ### put this at end of a module for self-auto-ordering (e.g. at import):
    try:
        __order
    except NameError:
        from util.assignment_order import ASTVisitor4assign
        ASTVisitor4assign().parseFile_of_module( __file__).set_order_in_namespace( globals(), flatten =True)
        #print '==================='
        #print __order

    ###get _all_ attribs of some class, ordered:
    for key,value in get_class_attributes_flatten_ordered( xxx.X1):
        print ' ', key,value

"""

# see compiler.visitor
# uses compiler.ast as parser.ast is too grammar specific.

class ASTVisitor4assign:
    """create an instance with optional call_on_duplicates functor argument,
    then run .parse*( src).
    a.parse( src1); a.parse( src2)   ===  a.parse( src1 + src2 )
    """
    class Klas:
        """ root instance of this is returned by ASTVisitor4assign.parse*().
        use set_order_in_namespace() to setattr a '__order' list into each class of the
        source class hierarhcy, assuming it exists as class object"""

        def __init__( me, name ):
            me.name = name
            me.vars = []
            me._vars = {}  #fast search
            me.klasi = []
            me.all = []
        def _add_var( me, name, call_on_duplicates =None, *args):
            if name in me._vars:
                if call_on_duplicates:
                    call_on_duplicates( name, *args)
            else:
                me.vars.append( name)
                me._vars[ name] = 1
                me.all.append( name)

        def _add_klas( me, klas ):
            #no check for duplicates
            me.klasi.append( klas)
            me.all.append( klas)
        _add_func = _add_var
        _add_import = _add_var

        def pprint( me, pfx ='', ):
            if me.all:
                print pfx, 'klas', me.name or '<>' ,':'
            pfx = pfx+'  '
            if 1:
                for var in me.vars:
                    print pfx, var
                for klas in me.klasi:
                    klas.pprint( pfx)
            else:
                for a in me.all:
                    if isinstance( a, me.__class__):
                        a.pprint( pfx)
                    else:
                        print pfx, var

        def set_order_in_namespace( me, namespace, order_name ='__order', flatten =False, **kargs_flatten_class__order ):
            """ assign a '__order' list into each class of the hierarchy, assuming it exists as class object.
                namespace is the relevant (for me) class, module or dict (e.g. globals()
                which is same as __main__.__dict__).
                flatten=True will pre-calculate the flattened-hierarchy attribute list (__order_flat).
            """
            order = me.vars
            if type( namespace) is dict:
                _setattr = dict.__setitem__
                _getattr = dict.__getitem__
            else:
                _setattr = setattr
                _getattr = getattr
            _setattr( namespace, order_name, order)
            for k in me.klasi:
                k_namespace = _getattr( namespace, k.name)
                k.set_order_in_namespace( k_namespace, order_name=order_name)
                if flatten:
                    flatten_class__order( k_namespace, order_name=order_name, **kargs_flatten_class__order)

        def pprint_order_in_namespace( me, namespace, order_name ='__order', pfx='', **kargs_flatten_class__order ):
            """ namespace is the relevant (for me) class, module or dict
                (e.g. globals() which is __main__.__dict__)
            """
            if type( namespace) is dict:
                _getattr = dict.__getitem__
                print pfx,'<dict>:'
            else:
                _getattr = getattr
                print pfx,str(namespace)+':'
            pfx = pfx+'   '

            print pfx, '- order:'
            print pfx,' ', _getattr( namespace, order_name)
            import types
            if type( namespace) is dict or isinstance( namespace, types.ModuleType): pass
            else:
                print pfx, '- flattened order:'
                for k,v in get_class_attributes_flatten_ordered( namespace, **kargs_flatten_class__order):
                    print pfx,' ', k,'\t=',v

            for klas in me.klasi:
                k_namespace = _getattr( namespace, klas.name)
                klas.pprint_order_in_namespace( k_namespace, order_name=order_name, pfx=pfx, **kargs_flatten_class__order)

    def __init__( me, call_on_duplicates =None, do_function =False, do_import =False):
        me.klas = me.Klas( '')    #None  #root
        me.call_on_duplicates = call_on_duplicates
        me.do_function = do_function
        me.do_import = do_import

    def visitAssName( me, node):
        #print node
        me.klas._add_var( node.name, me.call_on_duplicates, node, me )

    def visitClass( me, node):
        #print 'klas', node.name
        klas = me.klas
        name = node.name

        newklas = me.Klas( name)
        klas._add_klas( newklas)     #what if same klas repeated.. bad luck

        me.klas = newklas       #push
        me.visit( node.code)    #call back the caller - indirect recursion
        me.klas = klas          #pop

    #all things inside funcs are not of interest - jump over
    def visitFunction( me, node):
        #print 'func', node.name, 'ignored'
        if me.do_function:
            me.klas._add_func( node.name, me.call_on_duplicates, node, me )
        return

    def visitImport( me, node):
        #print 'import', node.names, 'ignored'
        if me.do_import:
            for mod_name, as_name in node.names:
                if as_name is None:
                    as_name = mod_name.split( '.',1)[0]
                me.klas._add_import( as_name, me.call_on_duplicates, node, me )
        return

    def visitFrom( me, node):
        #print 'from', node.modname, 'import', node.names, 'ignored'
        return me.visitImport( node)

    def parse( me, src):
        import compiler
        ast = compiler.parse( src +'\n')
        compiler.walk( ast, me )
        return me.klas

    def parseFile( me, path):
        import compiler
        ast = compiler.parseFile( path)
        compiler.walk( ast, me )
        return me.klas

    def parseModuleSource( me, module):
        """ Warning: module's .__file__ attribute may be pathname of
            precompiled .pyc, .pyo, shared library .so/.dll/.. for C extensions,
            or not available at all if module is statically linked into interpreter.
            These are all checked but Something may escape through...
        """
        try:
            src_filename = module.__file__
        except AttributeError:      #statically linked builtin
            raise TypeError, 'cannot access sourcefile for '+ str(module)
        return me.parseFile_of_module( src_filename)

    def parseFile_of_module( me, src_filename):
        """ Warning: module's .__file__ attribute may be pathname of
            precompiled .pyc, .pyo, shared library .so/.dll/.. for C extensions,
            or not available at all if module is statically linked into interpreter.
            These are all checked but Something may escape through...
        """
        #can use inspect.getsourcefile( config), but this is a bit better
        fl = src_filename.lower()   #to lower() or not??
        import imp
        for suffix, mode, kind in imp.get_suffixes():
            if fl[-len(suffix):] == suffix:
                if kind == imp.PY_COMPILED:
                    src_filename = src_filename[:-len(suffix)] + '.py'  #guess? lower/upper-case??
                elif kind != imp.PY_SOURCE:
                    # not text file
                    raise TypeError, 'cannot access sourcefile for '+ str(module)
                break
        return me.parseFile( src_filename) #hope for accessible, syntacticaly correct, source file


def flatten_class__order( klas, order_name ='__order', flat_name ='__order_flat',
                            ignore_missing_order =False, _used =None ):
    """collect and set order of _all_ attributes of (hierarchical) class as class.flat_name """
    #ignore cached __order_flat when computing
    if _used is None:   #root
        try:
            #return getattr( klas, flat_name)       #this looks through into bases!
            return klas.__dict__[ flat_name ]
        except (AttributeError, KeyError): pass
        _used = {},{}   #global for root
    _used_klas,_used_vars = _used

    o = []
    #print klas
    try:
        #if klas.__class__ is type: return o     #klas is a type-object
        if issubclass( klas, type): return o     #klas is a type-object
    except AttributeError: pass                 #non-object/old-style classes have no __class__

    #recursion, deep then wide...
    for base in klas.__bases__:
        if base not in _used_klas:      #base duplicates ignored
            _used_klas[ base] = 1
            o += flatten_class__order( base, order_name, flat_name, ignore_missing_order, _used )

    try:
        order = getattr( klas, order_name)
    except AttributeError:
        if not ignore_missing_order: raise
    else:
        for a in order:     #do not ignore missing __order
            if a not in _used_vars:         #vars duplicates ignored
                o.append( a)
                _used_vars[ a] = 1
        setattr( klas, flat_name, o )
    return o

def get_class_attributes_flatten_ordered( klas, inst =None, **kargs_flatten_class__order):
    """ordered yield attr,value for _all_ attributes of (hierarchical) class/instance """
    if inst is None: inst = klas
    for a in flatten_class__order( klas, **kargs_flatten_class__order ):
        yield a, getattr( inst, a)


def test( module, src_filename =None, flatten =True, print_tree =False):
    a = ASTVisitor4assign()
    if not src_filename:
        klastree = a.parseModuleSource( module)   #try guess the source file
    else:
        assert module
        klastree = a.parseFile_of_module( src_filename)   #hope for accessible, syntacticaly correct, source file

    if print_tree:
        print module, 'tree:'
        klastree.pprint()

    klastree.set_order_in_namespace( module, flatten =flatten)
    klastree.pprint_order_in_namespace( module)
    return klastree


if __name__=='__main__':

    s = """
p = int
class Record:
    Timestamp   = str
    ddd, ber, aaa = 2,3,4
    class SubRec:
        a,c = 5,6
        b =    12
    d = SubRec
    ddd = 43    #repeat
    import os
    import compiler.ast
    import compiler.ast as xxxx
    from compiler.ast import Node
    class ASubRec( SubRec, SubRec):
        z =    15
        b =    15
        def fun(x):
            z = 4
            return z
"""

    help = """usage:
        no args       - print and test some internal source text
        name          - if endswith .py, loads file as module 'autoname'
                        else import name as module
        -module name  - force import name as module
    """

    import sys
    name = None
    is_module = False
    for a in sys.argv[1:]:
        if a == '-module':
            is_module = True
        elif a.startswith( '-'):
            print help
            sys.exit(-1)
        else:
            name = a
            if not name.lower().endswith( '.py'):
                is_module = True

    a = ASTVisitor4assign( do_function= True, do_import =True)
    if name:
        if is_module:  #module name
            print 'source - module:', name
            module = __import__( name )
            klastree = a.parseFile_of_module( module)
        else:
            print 'source - file (as module "autoname"):', name
            klastree = a.parseFile( name)   #source_file_name
            import imp
            f = open( name)
            try:
                module = imp.load_module( 'autoname', f, name, ('.py', 'r', imp.PY_SOURCE) )
            finally:
                f.close()
    else:
        print 'source - text:', s
        print '-----'
        klastree = a.parse( s)
    #klastree.pprint()

    if not name:
        exec s
        import __main__
        module = __main__

    klastree.set_order_in_namespace( module)    #,flatten=True to pre-calc flattened

    klastree.pprint_order_in_namespace( module)
    if not name:
        f = flatten_class__order( Record.ASubRec)
        print Record.ASubRec,f
        assert f == [ 'a','c','b', 'z','fun']

# vim:ts=4:sw=4:expandtab
