# AttrDict Python module: a convenience wrapper to Python's dict.
# Author: Steven Brown <steven.w.j.brown@gmail.com>
# Homepage: http://stevenbrown.ca
# Copyright (C) 2008 Steven Brown

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""
Simple extension of the built-in dictionary so that dictionary keys are mirrored
as object attributes.  This is for convenience.  You can do things like this:

  d = dict()     #standard dict, for comparison
  a = AttrDict() #

  d['ok'] = 'this is ok'
  d.ok -> AttributeError raised
  d.ok = 'test' -> AttributeError raised

You cannot assign attributes to standard Python dicts.

  a['ok'] = 'this is ok'
  a.ok -> 'this is ok'    #attribute is automatically created
  a.ok = 'changed'
  a['ok'] -> 'changed'    #and the dict value is automatically updated
  a.ok2 = 'new value'     #adding new attribute, ok2
  a['ok2'] -> 'new value' #dict key is automatically created

This introduces a limitation on the dictionary keys such
that they must be strings and provide valid Python syntax for accessing.

For example:

  {'123':'valid'} #is a valid dictionary

but

  mydict.123 #is not valid Python syntax.

Attempting to create a key that cannot be accessed through an attribute name
will raise an AttributeError Exception.

This module has not been built for optmization.  Some of the method
documentation has been taken from Python's dict documentation.  For more info
on a particular method, refer to that.  I've tried to mirror the effects of
the standard dict as closely as possible.
"""

__version__  = "0.1"


# Create a namespace with an object to use for testing syntax
test_namespace = {}
exec ("class O(object):pass",test_namespace)
exec ("o = O()",test_namespace)
#FIXME - hide this further within the module?

class AttrDict(dict):
    
    def __init__(self, *args):
        """Takes an optional dict or AttrDict object to initialize with."""
        super(AttrDict,self).__init__()
        if len(args) > 1:
            raise TypeError("__init__ takes at most 1 argument (%i given)"
                            % len(args))
        elif len(args) == 1:
            d = dict(args[0])
            self.update(d)


    def update(self,*d,**F):
        """D.update(E, **F) -> None.  Update D from E and F: for k in E: D[k] = E[k]	 
        (if E has keys else: for (k, v) in E: D[k] = v) then: for k in F: D[k] = F[k]"""
        if len(d) < 1: pass

        elif len(d) > 1:
            raise TypeError("update expected at most 1 arguments, got %i" % len(d))
        
        else:
            d = d[0]
            if type(d) != dict:
                d = dict(d)
            for (k,v) in d.items():
                self[k] = v

        #kwargs
        for (k,v) in F.items():
            self[k] = v

        assert(self.__is_balanced())


    def clear(self):
        "D.clear() -> None.  Remove all items from D."
        for k in self.keys():
            del self[k]
        assert(self.__is_balanced())

    def __repr__(self):
        return "AttrDict(" + super(AttrDict,self).__repr__() + ")"

        
    def __getitem__(self, i):
        try:
            a = getattr(self,i)
        except AttributeError:
            raise KeyError(i)
        return a

    def __setitem__(self, i, val):
        "Sets the object's dictionary item i and attribute i to val.  \
        If i is not of type str, a TypeError is raised. \
        If i yields invalid attribute syntax, an AttributeError is raised."

        # test i to make sure it's a string...
        if type(i) != str:
            raise TypeError("Item key must be a string, not '%s'" % type(i))
                            
        # ...which yields valid attribute syntax
        if not self.__yieldsValidSyntax(i):
            raise AttributeError( \
                "Dictionary Key must valid attribute name. 'my_object.%s' is not valid syntax." % i)
        self.__setattr_helper(i, val)


    def __setattr__(self, a, val):
        if not self.__yieldsValidSyntax(a):
            raise AttributeError( \
                "Attribute name not valid. 'my_object.%s' is not valid syntax." % a)
        self.__setattr_helper(a, val)


    def __yieldsValidSyntax(self, a_str):
        """Returns True if a_str will be acceptable to python through the dot
        operator: my_object.<value of a_str> is possible.  Python keywords,"""
        try:
            #Pass in a namespace with pre-created object 'o'
            test_code = "o."+ a_str +" = 0; del o."+ a_str
            exec(test_code, test_namespace)
        except SyntaxError, e:
            return False
        return True


    def __setattr_helper(self, a, val):
        super(AttrDict,self).__setattr__(a,val)
        super(AttrDict,self).__setitem__(a,val)
        assert(self.__is_balanced())


    def __delattr__(self,a):
        super(AttrDict,self).__delattr__(a)
        super(AttrDict,self).__delitem__(a)
        assert(self.__is_balanced())


    def __delitem__(self,i):
        self.__delattr__(i)


    def get(self,k,d=None):
        """D.get(k[,d]) -> D[k] if k in D, else d.  d defaults to None."""
        return super(AttrDict,self).get(k,d)


    def setdefault(self,k,d=None):
        "D.setdefault(k[,d]) -> D.get(k,d), also set D[k]=d if k not in D"
        if self.has_key(k):
            return self.get(k,d)
        self[k] = d
        return d

        
    def pop(self, k, d=KeyError):
        """
        D.pop(k[,d]) -> v, remove specified key and return the corresponding valu
        If key is not found, d is returned if given, otherwise KeyError is raised
        """
        if d != KeyError:
            v = super(AttrDict,self).pop(k,d)

        else:
            v = super(AttrDict,self).pop(k)

        if hasattr(self, k):
            super(AttrDict,self).__delattr__(k)

        assert(self.__is_balanced())
        return v


    def popitem(self):
        """D.popitem() -> (k, v), remove and return some (key, value) pair as a
        2-tuple; but raise KeyError if D is empty"""
        if len(self.keys()) <= 0:
            raise KeyError("AttrDict is empty.")
        k = self.keys()[0]
        v = self.pop(k)
        assert(self.__is_balanced())
        return (k, v)


    #TODO def fromkeys():pass 
    #a.fromkeys(seq[, value])  	Creates a new dictionary with keys from seq and values set to value
    #fromkeys() is a class method that returns a new dictionary. value defaults to None. New in version 2.3.


    #TODO make public?
    def __is_balanced(self):
        """Returns True if the dictionary items and attributes are equal.  They
        should be at the end of all operations.  Returns False otherwise."""
        return vars(self) == dict(self)

    
    #FIXME - kill me, or make me beautiful
    @classmethod
    def is_synced(cls,attrdict,show=True):
        if type(attrdict) != AttrDict:
            raise TypeError("Expecting AttrDict type.")
        
        for key in attrdict.keys():
            if show:
                print "d[%s] is d.%s ('%s') :: " % \
                    (key, key, getattr(attrdict,key)),

            res = attrdict[key] is getattr(attrdict,key)
            if show: print res
            if not res:
                return False

        return True
    
