# ***** BEGIN LICENSE BLOCK *****
# Version: MPL 1.1/GPL 2.0/LGPL 2.1
#
# The contents of this file are subject to the Mozilla Public License Version
# 1.1 (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
# http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS IS" basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
# for the specific language governing rights and limitations under the
# License.
#
# The Original Code is the Python Computer Graphics Kit.
#
# The Initial Developer of the Original Code is Matthias Baas.
# Portions created by the Initial Developer are Copyright (C) 2004
# the Initial Developer. All Rights Reserved.
#
# Contributor(s):
#
# Alternatively, the contents of this file may be used under the terms of
# either the GNU General Public License Version 2 or later (the "GPL"), or
# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
# in which case the provisions of the GPL or the LGPL are applicable instead
# of those above. If you wish to allow use of your version of this file only
# under the terms of either the GPL or the LGPL, and not to allow others to
# use your version of this file under the terms of the MPL, indicate your
# decision by deleting the provisions above and replace them with the notice
# and other provisions required by the GPL or the LGPL. If you do not delete
# the provisions above, a recipient may use your version of this file under
# the terms of any one of the MPL, the GPL or the LGPL.
#
# ***** END LICENSE BLOCK *****
# $Id: mayaascii.py,v 1.10 2005/06/15 19:18:46 mbaas Exp $

import sys, types, simplecpp

# The keywords that may be used for the value True
_true_keywords = ["true", "on", "yes"]
# and for the value False
_false_keywords = ["false", "off", "no"]

# splitDAGPath
def splitDAGPath(path):
    """Split a Maya DAG path into its components.

    The path is given as a string that may have the form
    <namespace>:<path> where <path> is a sequence of strings
    separated by '|'.
    The return value is a 2-tuple (namespace, names) where namespace
    is None if the path did not contain a ':'. names is a list of
    individual path names.

    Examples:

    :time1   ->  ('', ['time1'])
    |foo|bar ->  (None, ['', 'foo', 'bar'])
    foo|bar  ->  (None, ['foo', 'bar'])
    """
    if not isinstance(path, types.StringTypes):
        raise ValueError, "string type expected as path argument, got %s"%type(path)
        
    namespace = None
    n = path.find(":")
    if n!=-1:
        namespace = path[:n]
        path = path[n+1:]
    return namespace, path.split("|")

# stripQuotes
def stripQuotes(s):
    """Remove surrounding quotes if there are any.

    The function returns the string without surrounding quotes
    (i.e. '"foo"' -> 'foo'). If there are no quotes the string
    is returned unchanged.
    """
    if s[0]=='"':
        return s[1:-1]
    else:
        return s
    

# MAPreProcessor
class MAPreProcessor(simplecpp.PreProcessor):
    """Preprocess the source file and invoke a callback for each line.
    """
    
    def __init__(self, linehandler):
        simplecpp.PreProcessor.__init__(self)
        self.linehandler = linehandler
        
    def output(self, s):
        # Ignore the preprocessor lines
        if s[0:1]!="#":
            continue_flag = self.linehandler(s)
            if not continue_flag:
                self.abort()

# PolyFace
class PolyFace:
    """Stores the data of a polyFace value.

    PolyFace objects are returned by the getValue() method of the
    Attribute class when the type was "polyFaces".
    """
    def __init__(self):
        # face (list of ints)
        self.f = None
        # holes (list of list of ints)
        # There's a list if vertex ids for each hole
        self.h = []
        # face tex coords (list of list of ints)
        self.mf = []
        # hole tex coords (list of list of ints)
        self.mh = []
        # tex coords (list of list of 2-tuple (uvset, list of ints))
        # There's a list for each loop (#f + #h).
        # Each list contains a list of 2-tuples (as there can be more than
        # one uvset per face)
        self.mu = []
        # face colors (list of list of ints)
        # There's a list for each loop (#f + #h)
        self.fc = []

    def __str__(self):
        return "<PolyFace %s #holes:%d>"%(self.f, len(self.h))

    # hasValidTexCoords
    def hasValidTexCoords(self):
        loops = [self.f]+self.h
        if len(self.mu)!=len(loops):
            return False
        for mus,loop in zip(self.mu, loops):
            if len(mus)==0:
                return False
            if len(mus[0][1])!=len(loop):
                return False

        return True

    # newLoop
    def newLoop(self):
        """This is an internal method used during construction of a poly face.

        The method has to be called after a new 'f' or 'h' attribute was
        encountered. The method will then allocate a new empty list for the
        texture coords and face colors. This list is then filled during
        construction.
        This ensures that the number of tex coords and color lists matches
        the number of loops.
        """
        self.mf.append([])
        self.mh.append([])
        self.mu.append([])
        self.fc.append([])

# NurbsCurve
class NurbsCurve:
    """Stores the data of a nurbsCurve value.

    NurbsCurve objects are returned by the getValue() method of the
    Attribute class when the type was "nurbsCurve".
    """
    def __init__(self):
        # Degree
        self.degree = 0
        # Spans
        self.spans = 0
        # Form attribute (0=open, 1=closed, 2=periodic)
        self.form = 0
        # Is the curve rational?
        self.isrational = False
        # Dimension (2 or 3)
        self.dimension = 0
        # Knots
        self.knots = []
        # Control vertices
        self.cvs = []

#    def __str__(self):
#        return "<NurbsCurve>"

# NurbsSurface
class NurbsSurface:
    """Stores the data of a nurbsSurface value.

    NurbsSurface objects are returned by the getValue() method of the
    Attribute class when the type was "nurbsSurface".
    """
    def __init__(self):
        # Degree in u and v
        self.udegree = 0
        self.vdegree = 0
        # Form attribute in u and v (0=open, 1=closed, 2=periodic)
        self.uform = 0
        self.vform = 0
        # Is the surface rational?
        self.isrational = False
        # Knots in u and v
        self.uknots = []
        self.vknots = []
        # Control vertices
        self.cvs = []

#    def __str__(self):
#        return "<NurbsSurface>"

# Attribute
class Attribute:
    """This class stores an attribute (i.e. the name and its value).

    An Attribute object is initialized with the arguments that were passed
    to the onSetAttr() callback of the reader class. The main purpose
    of an Attribute object is to convert the value into an appropriate
    Python value.
    """
    
    def __init__(self, attr, vals, opts):
        """Constructor.

        attr, vals and opts are the parameters of the onSetAttr() callback.
        """
        self._attr = stripQuotes(attr)
        self._vals = vals
        self._opts = opts

    def __str__(self):
        val = str(self._vals)
        if len(val)>10:
            val = val[:10]+"..."
        return "<Attribute %s %s %s>"%(self._attr, val, self._opts)

    # getBaseName
    def getBaseName(self):
        """Return the base name of the attribute.

        This is the first part of the attribute name (and may actually
        refer to another attribute).

        ".t"            -> "t"
        ".ed[0:11]"     -> "ed"
        ".uvst[0].uvsn" -> "uvst"
        """
        a = self._attr.split(".")
        b = a[1].split("[")
        return b[0]

    # getFullName
    def getFullName(self):
        return self._attr

    # getValue
    def getValue(self, type=None, n=None):
        """Return the converted value.

        type is a string containing the required type of the value.

        Valid types are "bool", "int", "float"
        "short2", "short3", "long2", "long3",
        "double2", "double3", "float2", "float3", "string", "int32Array",
        "doubleArray", "polyFaces", "nurbsSurface", "nurbsCurve",
        "double4", "float4" (for colors).
        """
        
        # Check if the value type was specified in the setAttr call
        valtype = self._opts.get("type", [None])[0]
        if valtype==None and type==None:
            if filter(lambda x: x in _true_keywords+_false_keywords, self._vals)!=[]:
                valtype = "bool"
            else:
                valtype = "float"

        # Use the real attribute type if no required type was specified
        if type==None:
            type = valtype

        # Use the provided type if the attribute had no 'type' option
        if valtype==None:
            valtype = type

        # No type information given?
        if valtype==None:
            raise ValueError, "No type information available for attribute '%s'"%self.getBaseName()

        # Check if the type matches the required type...
        if valtype!=type:
            raise ValueError, "Attribute of type %s expected, got %s"%(type, valtype)

        # Convert the values..
        convertername = "convert"+type[0].upper()+type[1:]
        f = getattr(self, convertername)
        try:
            vs = f()
        except ValueError, e:
            print >>sys.stderr, e
            # Try a string conversion when no type was specified...
            if type==None and convertername!="convertString":
                vs = self.convertString()
            else:
                raise
        if n==None:
            return vs
        if len(vs)!=n:
            raise ValueError, "%s: %d values expected, got %d"%(self._attr, n, len(vs))
        if n==1:
            return vs[0]
        else:
            return vs

    # The following convert methods have to convert the value list (self._vals)
    # into the appropriate type. The return value is always a list of values.

    def convertInt(self):
        return map(lambda x: int(x), self._vals)

    def convertFloat(self):
        return map(lambda x: float(x), self._vals)

    def convertBool(self):
        res = []
        for v in self._vals:
            res.append(v in _true_keywords)
        return res

    def convertLong2(self):
        res = []
        vs = self._vals
        for i in range(0,len(vs), 2):
            res.append((int(vs[i]), int(vs[i+1])))
        return res

    convertShort2 = convertLong2

    def convertLong3(self):
        res = []
        vs = self._vals
        for i in range(0,len(vs), 3):
            res.append((int(vs[i]), int(vs[i+1]), int(vs[i+2])))
        return res

    convertShort3 = convertLong3

    def convertDouble2(self):
        res = []
        vs = self._vals
        for i in range(0,len(vs), 2):
            res.append((float(vs[i]), float(vs[i+1])))
        return res

    convertFloat2 = convertDouble2

    def convertDouble3(self):
        res = []
        vs = self._vals
        for i in range(0,len(vs), 3):
            res.append((float(vs[i]), float(vs[i+1]), float(vs[i+2])))
        return res

    convertFloat3 = convertDouble3

    def convertDouble4(self):
        res = []
        vs = self._vals
        for i in range(0,len(vs), 4):
            res.append((float(vs[i]), float(vs[i+1]), float(vs[i+2]), float(vs[i+3])))
        return res

    convertFloat4 = convertDouble4

    def convertString(self):
        return map(lambda x: str(x), self._vals)

    def convertInt32Array(self):
        n = int(self._vals[0])
        return map(lambda x: int(x), self._vals[1:n+1])

    def convertDoubleArray(self):
        n = int(self._vals[0])
        return map(lambda x: float(x), self._vals[1:n+1])

    def convertPolyFaces(self):
        res = []
        vs = self._vals
        i=0
        pf = None
        while i<len(vs):
            c = vs[i]
            i+=1
            if c not in ["f", "h", "mf", "mh", "mu", "fc"]:
                raise ValueError, "Unknown polyFace data: %s"%c
            
            if c=="mu":
                uvset = int(vs[i])
                i+=1
                
            n = int(vs[i])
            ids = map(lambda x: int(x), vs[i+1:i+n+1])
            i+=n+1
            
            # Is that already a new polyFace? Then store the previous one
            # and start a new face
            if c=="f":
                if pf!=None:
                    res.append(pf)
                pf = PolyFace()
                pf.f = ids
                pf.newLoop()
            elif c=="h":
                pf.h.append(ids)
                pf.newLoop()
            elif c=="mu":
                pf.mu[-1].append((uvset, ids))
            else:
                # Set the IDs into the corresponding list
                lst = getattr(pf, c)
                lst[-1] = ids

        # Append the last face
        if pf!=None:
            res.append(pf)
            
        return res

    def convertNurbsCurve(self):
        res = []
        vs = self._vals

        while vs!=[]:
            nc = NurbsCurve()
            nc.degree = int(vs[0])
            nc.spans = int(vs[1])
            nc.form = int(vs[2])
            nc.isrational = vs[3] in _true_keywords
            nc.dimension = int(vs[4])
            # Get knots
            n = int(vs[5])
            nc.knots = map(lambda x: float(x), vs[6:n+6])
            vs = vs[n+6:]
            # Get CVs
            n = int(vs[0])
            dim = nc.dimension
            if nc.isrational:
                dim += 1
            cvs = []
            for i in range(n):
                cvs.append( tuple(map(lambda x: float(x), vs[1+i*dim:1+(i+1)*dim])) )
            nc.cvs = cvs
            vs = vs[1+n*dim:]
            res.append(nc)

        return res

    def convertNurbsSurface(self):
        res = []
        vs = self._vals

        while vs!=[]:
            ns = NurbsSurface()
            ns.udegree = int(vs[0])
            ns.vdegree = int(vs[1])
            ns.uform = int(vs[2])
            ns.vform = int(vs[3])
            ns.isrational = vs[4] in _true_keywords
            vs = vs[5:]
            # Get u knots
            n = int(vs[0])
            ns.uknots = map(lambda x: float(x), vs[1:n+1])
            vs = vs[n+1:]
            # Get v knots
            n = int(vs[0])
            ns.vknots = map(lambda x: float(x), vs[1:n+1])
            vs = vs[n+1:]
            # Skip TRIM|NOTRIM
            if vs[0] in ["TRIM", "NOTRIM"]:
                vs = vs[1:]
            # Get CVs
            n = int(vs[0])
            if ns.isrational:
                dim = 4
            else:
                dim = 3
            cvs = []
            for i in range(n):
                cvs.append( tuple(map(lambda x: float(x), vs[1+i*dim:1+(i+1)*dim])) )
            ns.cvs = cvs
            vs = vs[1+n*dim:]
            res.append(ns)

        return res
        
    # setValue
    def setValue(self, value):
        pass

# MultiAttrStorage
class MultiAttrStorage:
    """This helper class serves as MEL style array.

    You can assign values to arbitrary indices which will automatically
    enlarge the array (filling missing values with None).
    The slicing operation is different than Python as the stop index is
    inclusive. The actual array is stored in the _array attribute.

    Example:

    a = MultiAttrStorage()
    a[2] = 5               # -> [None, None, 5]
    a[4:6] = [1,2,3]       # -> [None, None, 5, None, 1, 2, 3]


    Reading an arbitrary attribute from an object of this class will
    automatically create this attribute which will in turn be of type
    MultiAttrStorage. So this class can also be used for compound objects.
    If you want to check such a compound for a particular attribute you
    must not use hasattr() as this would silently create the attribute if
    it didn't already exist and always return True. Instead you have to
    do the check as follows:

    if <attrname> in dir(compound):
       ...
    """
    def __init__(self):
        self._array = []

    def __str__(self):
        namedattrs = filter(lambda x: x[0:1]!="_", self.__dict__.keys())
        # Not an array but a 'struct' with named attributes...
        if namedattrs!=[] and self._array==[]:
            a = []
            for name in namedattrs:
                a.append(".%s:%s"%(name, getattr(self, name)))
            return ", ".join(a)
        else:
            return "["+", ".join(map(lambda x: str(x), self._array))+"]"

    def __iter__(self):
        return iter(self._array)

    def __len__(self):
        return len(self._array)

    def __getattr__(self, name):
        if name[:2]=="__":
            raise AttributeError, name
        ma = MultiAttrStorage()
        setattr(self, name, ma)
        return ma

    def __getitem__(self, key):
        if key>=len(self._array):
            ma = MultiAttrStorage()
            self.__setitem__(key, ma)
            return ma
        else:
            return self._array[key]

    def __setitem__(self, key, value):
        al = len(self._array)
        if type(key)==slice:
            if key.stop>=al:
                self._array += (key.stop-al+1)*[None]
            if len(value)!=(key.stop-key.start+1):
                raise ValueError, "%d values expected, got %d"%(key.stop-key.start+1, len(value))
            self._array[key.start:key.stop+1] = value
#            print key.start,key.stop
        else:
            if key<al:
                self._array[key] = value
            else:
                self._array += (key-al)*[None] + [value]

# Node
class Node:
    """A generic Maya node class.

    This is a helper class which may be used in a concrete implementation
    of the MAReader class to represent Maya nodes.

    This class does not implement the actual functionality of a
    particular Maya node, it just tracks attribute changes and
    connections which can later be retrieved once the entire file was
    read. So this class can be used for all Maya nodes in a file. 
    """
    
    def __init__(self, nodetype, opts, parent=None):
        """Constructor.

        nodetype and opts are the arguments of the onCreateNode()
        callback of the MAReader class.
        parent is the parent Node object or None.
        """

        # Do some type checking...
        if not isinstance(nodetype, types.StringTypes):
            raise ValueError, "Argument 'nodetype' must be a string, got %s."%(type(nodetype))
        if type(opts)!=dict:
            raise ValueError, "Argument 'opts' must be a dict, got %s."%(type(opts))
        if parent!=None and not isinstance(parent, Node):
            raise ValueError, "Argument 'parent' must be a Node object or None, got %s."%(type(parent))

        # A string containing the node type
        self.nodetype = nodetype
        # The options dictionary
        self.opts = opts

        # Parent Node object
        self._parent = None

        # Children Node objects
        self._children = []
        
        # Attribute values.
        # Key: Attribute base name / Value: List of Attribute objects
        self._setattr = {}

        # If True, accessing attributes will automatically create a new
        # attribute that contains a MultiAttrStorage object
        self._create_attributes = False

        # Key: Local attribute / Value: (nodename, attrname) of source
        self.in_connections = {}
        # Key: Local attribute / Value: List of (node, nodename, attrname) tuples
        self.out_connections = {}

        # Set the parent
        self.setParent(parent)

    def __str__(self):
        return '<Node "%s" %s>'%(self.getFullName(), self.nodetype)

    def __getattr__(self, name):
        if self._create_attributes:
            ma = MultiAttrStorage()
            setattr(self, name, ma)
            return ma
        raise AttributeError, name

    # getName
    def getName(self):
        """Return the node name.

        If no node name was specified during the creation of the object,
        the dummy name 'MayaNode' is returned.
        """
        return self.opts.get("name", ["MayaNode"])[0]

    # getFullName
    def getFullName(self):
        """Return the full node name.
        """
        name = self.getName()
        if self._parent==None:
            return "|%s"%name
        else:
            return "%s|%s"%(self._parent.getFullName(), name)

    # getParentName
    def getParentName(self):
        """Return the parent's node name or None.
        """
        if self._parent==None:
            return None
        else:
            return self._parent.getFullName()

    # getParent
    def getParent(self):
        """Return the parent Node object or None.
        """
        return self._parent

    # setParent
    def setParent(self, parent):
        """Reparent the Node object.
        """
        # Remove self from the previous parent's children list...
        if self._parent!=None:
            self._parent._children.remove(self)

        # Set the new parent...
        self._parent = parent
        if parent!=None:
            parent._children.append(self)

    # iterChildren
    def iterChildren(self):
        """Return an iterator that yields all children Node objects.
        """
        return iter(self._children)

    # setAttr
    def setAttr(self, attr, vals, opts):
        """Store the attribute value.

        The arguments are the same than the arguments of the onSetAttr()
        callback in the MAReader class.
        The final Python value can be retrieved with the getAttrValue()
        method.
        """
        # Is this setattr call only used to "declare" the size of an array?
        # then ignore the call
        if vals==[] and "size" in opts:
            return
        
        a = Attribute(attr, vals, opts)
        basename = a.getBaseName()
        if basename in self._setattr:
            self._setattr[basename].append(a)
        else:
            self._setattr[basename] = [a]

    # addAttr
    def addAttr(self, opts):
        pass

    # addInConnection
    def addInConnection(self, localattr, nodename, attrname):
        """Add an 'in' connection.

        'in' = node.attr is connected to localattr
        nodename is the name of a Node object and attrname the full
        attribute name.
        """
        self.in_connections[localattr] = (nodename, attrname)

    # addutConnection
    def addOutConnection(self, localattr, node, nodename, attrname):
        """Add an 'out' connection.

        'out' = localattr is connected to node.attr
        node is a Node object, nodename the name of the node and attrname
        the full attribute name.
        """
        if not isinstance(node, Node):
            raise ValueError, "Argument 'node' must be a Node instance."
        if self.out_connections.has_key(localattr):
            self.out_connections[localattr].append((node, nodename, attrname))
        else:
            self.out_connections[localattr] = [(node, nodename, attrname)]
        

    # getAttrValue
    def getAttrValue(self, lname, sname, type, n=1, default=None):
        """Get the Python value of an attribute.

        lname is the long name, sname the short name. type is the
        required type and n the required number of elements. type and
        n may be None.
        The return value is either a normal Python type (int, float, sequence)
        or a MultiAttrStorage object in cases where the setAttr command
        contained the index operator.
        When no attribute with the given long or short name could be
        found the provided default value is returned.

        This method will only return a value other than the default
        value when the \method{setAttr()} was called with the corresponding
        attribute.
        """
        # 'Execute' the stored setAttr commands for the long name and
        # the short name. This will store the value as an attribute
        # of self.
        self._executeAttrs(lname, type, n)
        self._executeAttrs(sname, type, n)
        # Check if the long name is available, otherwise try the short name...
        if hasattr(self, lname):
            return getattr(self, lname)
        return getattr(self, sname, default)

    # getInNode
    def getInNode(self, localattr_long, localattr_short):
        """Return the node and attribute that serves as input for localattr.

        The return value is a 2-tuple (nodename, attrname) that specifies
        the input connection for localattr. (None, None) is returned if there
        is no connection.
        """
        node, attr = self.in_connections.get(localattr_long, (None, None))
        if node==None:
            node, attr = self.in_connections.get(localattr_short, (None, None))
        return node, attr

    # getOutNodes
    def getOutNodes(self, localattr_long, localattr_short):
        """Return the nodes and attributes that this attribute connects to.

        The return value is a list of 3-tuples (node, nodename, attrname) that
        specify the output connections for localattr.
        """
        lst = self.out_connections.get(localattr_long, None)
        if lst==None:
            lst = self.out_connections.get(localattr_short, [])
        return lst

    # getOutAttr
    def getOutAttr(self, localattr_long, localattr_short, dstnodetype):
        """Check if a local attribute is connected to a particular type of node.

        Returns a tuple (node, attrname) where node is the Node object
        of the destination node and attrname the name of the destination
        attribute. If there is no connection with a node of type dstnodetype,
        the method returns (None,None).
        If the attribute is connected to more than one node with the
        given type or to several attributes of the same node then only
        the first connection encountered is returned.
        """
        cs = self.getOutNodes(localattr_long, localattr_short)

        # Search the connections for one that goes to a node with
        # the specified type...
        for node,nodename,attrname in cs:
            if node.nodetype==dstnodetype:
                return node, attrname
        return None,None

    # _executeAttrs
    def _executeAttrs(self, basename, type, n):
        """Retrieve the Python values out of an attribute.

        All stored attributes with the given basename will be 'executed'.
        This means, their Python value will be obtained and stored
        in the node under the same name than the original attribute.
        type is the required type of the attribute (may be None) and
        n the required number of elements (or None = Dynamic array).
        """
        self._create_attributes = True
        efftype = type
        for attr in self._setattr.get(basename, []):
            # Keep the type information once an attribute explicitly
            # provided some info. Later attributes that don't specify
            # the type (as can happen with the texture coordinates) can
            # then be read properly.
            if type==None:
                valtype = attr._opts.get("type", [None])[0]
                if valtype!=None:
                    efftype = valtype
            v = attr.getValue(efftype, n)
            # Unpack the value if the number of elements wasn't known and
            # it turned out to be only one element (in this case, the below
            # assignment will be of the form a[i]=v instead of a[i:j]=v)
            if n==None and len(v)==1:
                v = v[0]
            if attr.getBaseName() in ["in", "from", "for", "if", "while",
                                      "else", "elif", "exec"]:
                cmd = "setattr(self, '%s', v)"%attr.getFullName()[1:]
            else:
                cmd = "self%s = v"%attr.getFullName()
            if v!=[]:
                exec cmd
        self._create_attributes = False

######################################################################

# MAReader
class MAReader:
    """Low level MA (Maya ASCII) reader.

    The MAReader class reads Maya ASCII files and calls handler
    methods which have to be implemented in a derived class. The
    content of the file is actually a subset of the Maya Embedded
    Language (MEL) which is the scripting language implemented inside
    Maya. The MAReader parses the file, breaks down the content of the
    file in commands and their arguments and options (expressions are
    not evaluated). Each MEL command will then trigger a callback
    method that has to execute the command. These callback methods
    have to be implemented in a derived class.

    There are 12 MEL commands that can appear in a Maya ASCII file: 

    - file 
    - requires 
    - fileInfo 
    - currentUnit 
    - createNode 
    - setAttr 
    - addAttr 
    - connectAttr 
    - disconnectAttr 
    - parent 
    - select
    - lockNode

    Each command has a number of arguments and can also take
    options. The callback methods receive the arguments as regular
    arguments to the method and the options as an additional argument
    opts which is a dictionary containing the options that were
    specified in the file. The key is the long name of the option
    (without leading dash) and the value is a list of strings
    containing the option values. The number of values and how they
    have to be interpreted depend on the actual option.
    
    The callbacks may access a few instance variables that carry
    further information:
    
    - filename: The name of the ma file
    - cmd_start_linenr: The line number where the current command began
    - cmd_end_linenr: The line number where the current command ended 
    """
    
    def __init__(self):

        # createNode options
        self.createNode_name_dict = { "n":"name", "p":"parent", "s":"shared" }
        self.createNode_opt_def = { "name" : (1, None),
                                    "parent" : (1, None),
                                    "shared" : (0, None) }

        # setAttr options
        self.setAttr_name_dict = { "k":"keyable",
                                   "l":"lock",
                                   "s":"size",
                                   "typ":"type",
                                   "av":"alteredValue",
                                   "c":"clamp" }
        self.setAttr_opt_def = { "keyable" : (1, None),
                                 "lock" : (1, None),
                                 "size" : (1, None),
                                 "type" : (1, None),
                                 "alteredValue" : (0, None),
                                 "clamp" : (0, None)}

        # fileInfo options
        self.fileInfo_name_dict = { "rm":"remove" }
        self.fileInfo_opt_def = { "remove" : (1, None)}

        # currentUnit options
        self.currentUnit_name_dict = { "l":"linear",
                                       "a":"angle",
                                       "t":"time",
                                       "f":"fullName",
                                       "ua":"updateAnimation"}
        self.currentUnit_opt_def = { "linear" : (1, None),
                                     "angle" : (1, None),
                                     "time" : (1, None),
                                     "fullName" : (0, None),
                                     "updateAnimation" : (1, None)}

        # connectAttr options
        self.connectAttr_name_dict = { "l":"lock",
                                       "f":"force",
                                       "na":"nextAvailable",
                                       "rd":"referenceDest" }
        self.connectAttr_opt_def = { "lock" : (1, None),
                                     "force" : (0, None),
                                     "nextAvailable" : (0, None),
                                     "referenceDest" : (1, None) }

        # disconnectAttr options
        self.fileInfo_name_dict = { "na":"nextAvailable" }
        self.fileInfo_opt_def = { "nextAvailable" : (0, None)}

        # parent options
        self.parent_name_dict = { "w":"world",
                                  "r":"relative",
                                  "a":"absolute",
                                  "add":"addObject",
                                  "rm":"removeObject",
                                  "s":"shape",
                                  "nc":"noConnections" }
        self.parent_opt_def = { "world" : (0, None),
                                "relative" : (0, None),
                                "absolute" : (0, None),
                                "addObject" : (0, None),
                                "removeObject" : (0, None),
                                "shape" : (0, None),
                                "noConnections" : (0, None) }

        # select options
        self.select_name_dict = { "adn":"allDependencyNodes",
                                  "ado":"allDagObjects",
                                  "vis":"visible",
                                  "hi":"hierarchy",
                                  "af":"addFirst",
                                  "r":"replace",
                                  "d":"deselect",
                                  "tgl":"toggle",
                                  "cl":"clear",
                                  "ne":"noExpand" }
        self.select_opt_def = { "all" : (0, None),
                                "allDependencyNodes" : (0, None),
                                "allDagObjects" : (0, None),
                                "visible" : (0, None),
                                "hierarchy" : (0, None),
                                "add" : (0, None),
                                "addFirst" : (0, None),
                                "replace" : (0, None),
                                "deselect" : (0, None),
                                "toggle" : (0, None),
                                "clear" : (0, None),
                                "noExpand" : (0, None) }

        # addAttr options
        self.addAttr_name_dict = { "ln":"longName",
                                   "sn":"shortName",
                                   "bt":"binaryTag",
                                   "at":"attributeType",
                                   "dt":"dataType",
                                   "dv":"defaultValue",
                                   "m":"multi",
                                   "im":"indexMatters",
                                   "min":"minValue",
                                   "hnv":"hasMinValue",
                                   "max":"maxValue",
                                   "hxv":"hasMaxValue",
                                   "ci":"cachedInternally",
                                   "is":"internalSet",
                                   "p":"parent",
                                   "nc":"numberOfChildren",
                                   "uac":"usedAsColor",
                                   "h":"hidden",
                                   "r":"readable",
                                   "w":"writable",
                                   "s":"storable",
                                   "k":"keyable",
                                   "smn":"softMinValue",
                                   "hsn":"hasSoftMinValue",
                                   "smx":"softMaxValue",
                                   "hsx":"hasSoftMaxValue",
                                   "en":"enumName" }
        self.addAttr_opt_def = { "longName" : (1, None),
                                 "shortName" : (1, None),
                                 "binaryTag" : (1, None),
                                 "attributeType" : (1, None),
                                 "dataType" : (1, None),
                                 "defaultValue" : (1, None),
                                 "multi" : (0, None),
                                 "indexMatters" : (1, None),
                                 "minValue" : (1, None),
                                 "hasMinValue" : (1, None),
                                 "maxValue" : (1, None),
                                 "hasMaxValue" : (1, None),
                                 "cachedInternally" : (1, None),
                                 "internalSet" : (1, None),
                                 "parent" : (1, None),
                                 "numberOfChildren" : (1, None),
                                 "usedAsColor" : (0, None),
                                 "hidden" : (1, None),
                                 "readable" : (1, None),
                                 "writable" : (1, None),
                                 "storable" : (1, None),
                                 "keyable" : (1, None),
                                 "softMinValue" : (1, None),
                                 "hasSoftMinValue" : (1, None),
                                 "softMaxValue" : (1, None),
                                 "hasSoftMaxValue" : (1, None),
                                 "enumName" : (1, None) }

        # file options (incomplete)
        self.file_name_dict = { "bls":"buildLoadSettings",
                                "c":"command",
                                "dns":"defaultNamespace",
                                "dr":"deferReference",
                                "f":"force",
                                "fr":"flushReference",
                                "gl":"groupLocator",
                                "gn":"groupName",
                                "gr":"groupReference",
                                "ir":"importReference",
                                "lck":"lockReference",
                                "lf":"lockFile",
                                "lrd":"loadReferenceDepth",
                                "lad":"loadAllDeferred",
                                "lar":"loadAllReferences",
                                "lnr":"loadNoReferences",
                                "lr":"loadReference",
                                "ls":"loadSettings",
                                "pr":"preserveReferences",
                                "new":"newFile",
                                "o":"open",
                                "op":"options",
                                "pmt":"prompt",
                                "r":"reference",
                                "ra":"renameAll",
                                "rdi":"referenceDepthInfo",
                                "rfn":"referenceNode",
                                "rpr":"renamingPrefix",
                                "shd":"sharedNodes",
                                "sns":"swapNamespace",
                                "srf":"sharedReferenceFile",
                                "str":"strict",
                                "ns":"namespace",
                                # The following flags are not documented in the Maya docs
                                "pm":"proxyManager",
                                "pt":"proxyTag",
                                "ap":"activeProxy" }
        
        self.file_opt_def = { "buildLoadSettings" : (0, None), 
                              "command" : (1, None),
                              "defaultNamespace" : (0, None),
                              "deferReference" : (1, None),
                              "force" : (0, None),
                              "flushReference" : (1, None),
                              "groupLocator" : (0, None),
                              "groupName" : (1, None),
                              "groupReference" : (0, None),
                              "importReference" : (0, None),
                              "lockReference" : (0, None),
                              "lockFile" : (1, None),
                              "loadReferenceDepth" : (1, None),
                              "loadAllDeferred" : (1, None),
                              "loadAllReferences" : (0, None),
                              "loadNoReferences" : (0, None),
                              "loadReference" : (1, None),
                              "loadSettings" : (1, None),
                              "preserveReferences" : (0, None),
                              "newFile" : (0, None),
                              "open" : (0, None),
                              "options" : (1, None),
                              "prompt" : (1, None),
                              "reference" : (0, None),
                              "renameAll" : (1, None),
                              "referenceDepthInfo" : (1, None),
                              "referenceNode" : (1, None),
                              "renamingPrefix" : (1, None),
                              "sharedNodes" : (1, None),
                              "swapNamespace" : (2, None),
                              "sharedReferenceFile" : (0, None),
                              "strict" : (1, None),
                              "namespace" : (1, None),
                              "proxyManager" : (1, None),
                              "proxyTag" : (1, None),
                              "activeProxy" : (0, None) }

        # lockNode options
        self.lockNode_name_dict = { "l":"lock",
                                    "ic":"ignoreComponents" }
        self.lockNode_opt_def = { "lock" : (1, None),
                                  "ignoreComponents" : (0, None) }
        
    # Provide linenr as an alias for cmd_start_linenr
    @property
    def linenr(self):
        return self.cmd_start_linenr
    
    def read(self, f):
        """Read a MA file and invoke the callbacks.

        f is a file-like object or the name of a file.
        """
        self.begin()
        if isinstance(f, types.StringTypes):
            self.filename = f
        else:
            self.filename = getattr(f, "name", "?")
        # A flag that indicates if a new MEL command is about to begin
        self.new_cmd = True
        # The name of the current MEL command
        self.cmd = None
        # The arguments of the current MEL command
        self.args = None
        # This flag specifies whether reading the file should continue or not
        self.continue_flag = True

        # The line number where the current MEL command began
        self.cmd_start_linenr = None
        # The line number where the current MEL command ended
        self.cmd_end_linenr = None
        
        cpp = MAPreProcessor(self.lineHandler)
        self.cpp = cpp
        # Read the file and invoke the lineHandler for each line...
        cpp(f)
       
        # Execute the last command
        self.processCommands(";")
        self.end()

    def lineHandler(self, s):
#        self.linenr += 1
        z = s.strip()
        if z!="":
            self.processCommands(z)
        return self.continue_flag
    
    def abort(self):
        """Stop reading the MA file.
        
        This method can be called by a callback method to abort
        reading the file.
        """
        self.continue_flag = False

    def begin(self):
        """Callback that is invoked before the file is read."""
        pass

    def end(self):
        """Callback that is invoked after the file was read."""
        pass

    def onFile(self, filename, opts):
        """Callback for the 'file' MEL command."""
        pass
#        print "file", filename, opts

    def onRequires(self, product, version):
        """Callback for the 'requires' MEL command."""
        pass
#        print "requires",product, version

    def onFileInfo(self, keyword, value, opts):
        """Callback for the 'fileInfo' MEL command."""
        pass
#        print "fileInfo",keyword, value

    def onCurrentUnit(self, opts):
        """Callback for the 'currentUnit' MEL command."""
        pass
#        print "currentUnit",opts

    def onCreateNode(self, nodetype, opts):
        """Callback for the 'createNode' MEL command."""
        pass
#        print "createNode", nodetype, opts

    def onSetAttr(self, attr, vals, opts):
        """Callback for the 'setAttr' MEL command."""
        pass
#        print "setAttr %s = %s %s"%(attr, vals, opts)

    def onConnectAttr(self, srcattr, dstattr, opts):
        """Callback for the 'connectAttr' MEL command."""
        pass
#        print "connectAttr %s %s %s"%(srcattr, dstattr, opts)

    def onDisconnectAttr(self, srcattr, dstattr, opts):
        """Callback for the 'disconnectAttr' MEL command."""
        pass
#        print "disconnectAttr %s %s %s"%(srcattr, dstattr, opts)

    def onAddAttr(self, opts):
        """Callback for the 'addAttr' MEL command."""
        pass
#        print "addAttr", opts

    def onParent(self, objects, parent, opts):
        """Callback for the 'parent' MEL command."""
        pass
#        print "parent",objects,parent,opts

    def onSelect(self, objects, opts):
        """Callback for the 'select' MEL command."""
        pass
#        print "select",objects,opts

    def onLockNode(self, objects, opts):
        """Callback for the 'lockNode' MEL command.
        
        objects is a list of objects (which may be empty).
        """
        pass
#        print "lockNode",objects,opts

    # onCommand
    def onCommand(self, cmd, args):
        """Generic command callback.

        This callback invokes the "per command" callbacks.
        
        cmd is the MEL command name and args is a list of strings
        that are the arguments of the command. The arguments have
        been split into their individual tokens. Quotes around
        quoted tokens are still present.
        
        Example:
        
        The MEL command setAttr -k off ".v"; would be passed in
        as onCommand('setAttr', ['-k', 'off', '"v"'])
        """
#        print "**",cmd, args
        # setAttr
        if cmd=="setAttr":
            args, opts = self.getOpt(args,
                                     self.setAttr_opt_def,
                                     self.setAttr_name_dict)
            self.onSetAttr(args[0], args[1:], opts)
        # createNode
        elif cmd=="createNode":
            args, opts = self.getOpt(args,
                                     self.createNode_opt_def,
                                     self.createNode_name_dict)
            self.onCreateNode(args[0], opts)
        # connectAttr
        elif cmd=="connectAttr":
            args, opts = self.getOpt(args,
                                     self.connectAttr_opt_def,
                                     self.connectAttr_name_dict)
            self.onConnectAttr(args[0], args[1], opts)
        # disconnectAttr
        elif cmd=="disconnectAttr":
            args, opts = self.getOpt(args,
                                     self.disconnectAttr_opt_def,
                                     self.disconnectAttr_name_dict)
            self.onDisconnectAttr(args[0], args[1], opts)
        # addAttr
        elif cmd=="addAttr":
            args, opts = self.getOpt(args,
                                     self.addAttr_opt_def,
                                     self.addAttr_name_dict)
            self.onAddAttr(opts)
        # parent
        elif cmd=="parent":
            args, opts = self.getOpt(args,
                                     self.parent_opt_def,
                                     self.parent_name_dict)
            self.onParent(args[:-1], args[-1], opts)
        # select
        elif cmd=="select":
            args, opts = self.getOpt(args,
                                     self.select_opt_def,
                                     self.select_name_dict)
            self.onSelect(args, opts)
        # fileInfo
        elif cmd=="fileInfo":
            args, opts = self.getOpt(args,
                                     self.fileInfo_opt_def,
                                     self.fileInfo_name_dict)
            self.onFileInfo(args[0], args[1], opts)
        # currentUnit
        elif cmd=="currentUnit":
            args, opts = self.getOpt(args,
                                     self.currentUnit_opt_def,
                                     self.currentUnit_name_dict)
            self.onCurrentUnit(opts)
        # requires
        elif cmd=="requires":
            args, opts = self.getOpt(args, {}, {})
            self.onRequires(args[0], args[1])
        # file
        elif cmd=="file":
            args, opts = self.getOpt(args,
                                     self.file_opt_def,
                                     self.file_name_dict)
            self.onFile(args[0], opts)
        # lockNode
        elif cmd=="lockNode":
            args, opts = self.getOpt(args,
                                     self.lockNode_opt_def,
                                     self.lockNode_name_dict)
            self.onLockNode(args, opts)
        # unknown
        else:
            print >>sys.stderr, "WARNING: %s, line %d: Unknown MEL command: '%s'"%(self.filename, self.cmd_start_linenr, cmd)


    # getOpt
    def getOpt(self, arglist, opt_def, name_dict):
        """Separate arguments from options and preprocess options.

        arglist is a list of arguments (i.e. the 'command line').
        opt_def specifies the available options and their respective
        number of arguments. name_dict is a dictionary that is used
        to convert short names into long names.

        The return value is a 2-tuple (args, opts) where args is a list
        of arguments and opts is a dictionary containing the options.
        The key is the long name of the option (without leading dash)
        and the value is a list of values. Any existing quotes around
        a value is removed (only around the option values, not around
        the args!).
        """

        args = []
        opts = {}
        
        i=0
        while i<len(arglist):
            arg = arglist[i]
            i += 1
            try:
                float(arg)
                is_number = True
            except:
                is_number = False
            # Option?
            a = stripQuotes(arg)
            if a[0:1]=="-" and not is_number:
                # Convert short names into long names...
                optname = name_dict.get(a[1:], a[1:])
                # Check if the option is known
                if optname not in opt_def:
                    raise SyntaxError, "Unknown option in line %d: %s"%(self.cmd_start_linenr, optname)
                # Get the number of arguments
                numargs, filter = opt_def[optname]
                optvals = [stripQuotes(x) for x in arglist[i:i+numargs]]
                # Did the same option already appear? So this is a multi-use flag.
                # Then extend the current list with the new values
                if optname in opts:
                    opts[optname].extend(optvals)
                else:
                    opts[optname] = optvals
                i += numargs
            else:
                args.append(arg)

        return args, opts

    # processCommands
    def processCommands(self, s):
        """Process one or more commands.

        s is a string that contains one line of MEL code (may be several
        commands or only a partial command that is continued in the next
        line). This method splits the arguments and calls onCommand()
        for every command found.
        """
        # Split the command into tokens...
        a,n = self.splitCommand(s)
        if a!=[]:
            # Does a new command begin? then set the command name
            # and the args, otherwise just append to the existing args
            if self.new_cmd:
                self.cmd = a[0]
                self.args = a[1:]
                # Store the line number where the command began
                self.cmd_start_linenr = self.cpp.context.start_linenr
            else:
                self.args += a

        if n==-1:
            # The command isn't finished yet
            if self.cmd!=None:
                self.new_cmd = False
        else:
            # The command is finished, so execute it
            if self.cmd!=None:
                # Store the line number where the command ended
                self.cmd_end_linenr = self.cpp.context.linenr
                self.onCommand(self.cmd, self.args)
            self.new_cmd = True
            self.cmd = None
            self.args = []
            self.processCommands(s[n+1:])

    # splitCommand
    def splitCommand(self, s):
        """Split a command into its arguments.

        This is an extended version of the string split() method. It
        splits (using whitespace as separator) but takes quoted strings
        and ';' into account.
        Returns a list of strings and the position of the ';'
        that terminated the first command (or -1).
        The quotes around strings are kept.

        'setAttr -k off ".v";' -> (['setAttr', '-k', 'off', '".v"'], 19)
        """
        # Search for a quoted string
        b,e = self.findString(s)
        # Search for the first semicolon (which might be the true command
        # separator or not)
        n = s.find(";")
        # Was a semicolon before the first string? Then the string belongs
        # to a subsequent command, so ignore it for now
        if n!=-1 and n<b:
            b = e = None
        # No string found?
        if b==None:
            if n==-1:
                return s.split(), -1
            else:
                return s[:n].split(), n
        else:
            if e==None:
                return s[:b].split() + [s[b:]+'"'], -1
            else:
                s2,n = self.splitCommand(s[e+1:])
                if n!=-1:
                    n += e+1
                return s[:b].split() + [s[b:e+1]] + s2, n

    # findString
    def findString(self, s):
        """Find the first string occurence.

        The return value is a 2-tuple (begin, end) with the indices
        of the opening and closing apostrophes (can also be None).

        'foo'             -> (None, None)
        'a="foo"'         -> (2, 6)
        'a="foo'          -> (2, None)
        'a="foo \" spam"' -> (2,14)
        """
        #'
        offset = 0
        while 1:
            # Search the beginning of a string
            n1 = s.find('"', offset)
            if n1==-1:
                return None,None
            # Search the end of the string (ignore quoted apostrophes)...
            start = n1+1
            n2 = None
            while 1:
                n2 = s.find('"', start)
                if n2==-1:
                    return n1,None
                elif s[n2-1]!='\\':
                    return n1,n2
                else:
                    start = n2+1

# DefaultMAReader
class DefaultMAReader(MAReader):
    """Default MA reader implementation.

    This class creates Node objects, sets attributes and does the
    connections so that after the file is read the entire dependency
    graph is available.

    A derived class only has to implement the end() callback and
    process the graph as desired. All created Node objects are available
    in the attribute self.nodelist.
    """

    def read(self, f):
        # A dict with imported Node objects
        # Key: Node name (without path) / Value: Node object
        # If the node name is not unique anymore, the value contains None.
        self.nodes = {}
        
        # A list with all Node objects (in the same order as they were
        # encountered in the file)
        self.nodelist = []
        
        # The currently active Node object
        # (changes with every createNode or select command)
        self.currentnode = None

        MAReader.read(self, f)

    # onCreateNode
    def onCreateNode(self, nodetype, opts):
        """Create a new node and make it current.
        """
        # Remove all quotes...
        nodetype = stripQuotes(nodetype)
#        for name in opts:
#            opts[name] = map(lambda x: stripQuotes(x), opts[name])
            
        node = self.createNode(nodetype, opts)
        self.currentnode = node

    # onSelect
    def onSelect(self, objects, opts):
        """Make another node current."""
        # Remove all quotes...
        objects = map(lambda x: stripQuotes(x), objects)
        
        if opts!={"noExpand":[]}:
            raise ValueError, "%s, %d: The select command contains unsupported options."%(self.filename, self.linenr)

        if len(objects)==0:
            raise ValueError, "%s, %d: The select command contains no object name."%(self.filename, self.linenr)
        if len(objects)!=1:
            raise ValueError, "%s, %d: The select command contains more than one object."%(self.filename, self.linenr)

        self.currentnode = self.findNode(objects[0], create=True)

    # onSetAttr
    def onSetAttr(self, attr, vals, opts):
        """Set an attribute."""
        if self.currentnode==None:
            return

        # Remove the quotes...
        attr = stripQuotes(attr)
#        for name in opts:
#            opts[name] = map(lambda x: stripQuotes(x), opts[name])

        if attr[0]!=".":
            print >>sys.stderr, "mayaascii: Warning: DefaultMAReader.onSetAttr(): The attribute refers to a different object than the current object. This is not yet supported."

        self.currentnode.setAttr(attr, vals, opts)

    # onAddAttr
    def onAddAttr(self, opts):
        """Add a dynamic attribute."""
        if self.currentnode==None:
            return

        # Remove the quotes...
#        for name in opts:
#            opts[name] = map(lambda x: stripQuotes(x), opts[name])

        self.currentnode.addAttr(opts)

    # onConnectAttr
    def onConnectAttr(self, srcattr, dstattr, opts):
        """Make a connection.
        """

        # Remove the quotes...
        srcattr = stripQuotes(srcattr)
        dstattr = stripQuotes(dstattr)
#        for name in opts:
#            opts[name] = map(lambda x: stripQuotes(x), opts[name])

        # Split into object name and attribute name
        a = srcattr.split(".")
        snode = a[0]
        sattr = a[1]
        b = dstattr.split(".")
        dnode = b[0]
        dattr = b[1]
        
        sn = self.findNode(snode, create=True)
        dn = self.findNode(dnode, create=True)
        if sn!=None:
            if dn==None:
                print >>sys.stderr, 'WARNING: %s, %d: connectAttr "%s" "%s"'%(self.filename, self.linenr, srcattr, dstattr)
                print >>sys.stderr, ' Node "%s" not found. The connection is ignored.'%dnode
            else:
                sn.addOutConnection(sattr, dn, dnode, dattr)
        if dn!=None:
            dn.addInConnection(dattr, snode, sattr)


    # findNode
    def findNode(self, path, create=False):
        """Return the Node object corresponding to a particular path.

        path may also be None in which case None is returned.
        If create is True, any missing nodes are automatically created.

        (this method doesn't handle namespaces yet)
        """
        if path==None:
            return None
        
        namespace,names = splitDAGPath(path)
        # The current node (and eventually the result)
        node = None
        # Iterate over all names from 'top' to 'bottom'...
        for name in names:
            # An empty name? Then start from the beginning
            if name=="":
                node = None
            else:
                if node==None:
                    node = self.nodes.get(name)
                    if node==None:
                        if create:
                            node = self.createNode("<unknown>", {"name":[name]})
                        else:
                            raise KeyError, "Node %s not found (%s is missing)"%(path, name)
                else:
                    for cn in node.iterChildren():
                        if name==cn.getName():
                            node = cn
                            break
                    else:
                        if create:
                            node = self.createNode("<unknown>", {"name":[name], "parent":[node.getFullName()]})
                        else:
                            raise KeyError, "Node %s not found (%s is missing)"%(path, name)
        return node

    # createNode
    def createNode(self, nodetype, opts):
        """Create a new node and return it.
        """
        parentname = opts.get("parent", [None])[0]
        parent = self.findNode(parentname)
        node = Node(nodetype, opts, parent=parent)
        # The constant default name (if there was no name set in the file)
        # will override a previous node without name. But this doesn't
        # matter as the node cannot be addressed by name in the file anyway.
        nodename = node.getName()
        self.nodes[nodename] = node
        self.nodelist.append(node)
        return node
        


######################################################################

if __name__=="__main__":

    class TestReader(DefaultMAReader):

        def end(self):
            for node in self.nodelist:
                print '%-30s %-20s parent:%s'%('"'+node.getFullName()+'"', node.nodetype, node.getParentName())
                for attrname in node._setattr:
                    try:
                        val = node.getAttrValue(attrname, attrname, None, None)
                    except:
                        val = "<error retrieving value, need more type information>"
                    val = str(val)
                    if len(val)>60:
                        val = val[:60]+"..."
                    print "  %s = %s"%(attrname, val)
                


    maname = sys.argv[1]
    rd = TestReader()
    rd.read(maname)
