"""NamedValueAccess.py

NamedValueAccess provides functions for accessing Python objects by keys and
named attributes. A 'key' is a single identifier such as 'foo'. A 'name' could
be a key, or a qualified key, such as 'foo.bar.boo'. Names are generally more
convenient and powerful, while the key-oriented function is more efficient and
provide the atomic functionality that the name-oriented function is built upon.


CREDIT

Chuck Esterbrook <echuck@mindspring.com>
Tavis Rudd <tavis@calrudd.com>

"""


from types import MethodType
from UserDict import UserDict

from MiscUtils import NoDefault


## Exceptions ##

class NamedValueAccessError(LookupError):
    """General named value access error."""


class ValueForKeyError(NamedValueAccessError):
    """No value for key found error."""


## Functions ##

def valueForKey(obj, key, default=NoDefault):
    """Get the value of the object named by the given key.

    This method returns the value with the following precedence:
      1. Methods before non-methods
      2. Attributes before keys (__getitem__)
      3. Public things before private things
         (private being denoted by a preceding underscore)

    Suppose key is 'foo', then this method returns one of these:
      * obj.foo()
      * obj._foo()
      * obj.foo
      * obj._foo
      * obj['foo']
      * default # only if specified

    If all of these fail, a ValueForKeyError is raised.

    NOTES

      * valueForKey() works on dictionaries and dictionary-like objects.
      * See valueForName() which is a more advanced version of this
        function that allows multiple, qualified keys.

    """

    assert obj is not None

    # We only accept strings for keys
    assert isinstance(key, basestring)

    attr = method = None
    unknown = False
    if isinstance(obj, (dict, UserDict)):
        if default is NoDefault:
            try:
                return obj[key]
            except KeyError:
                raise ValueForKeyError(key)
        else:
            return obj.get(key, default)
    else:
        try:
            klass = obj.__class__
        except AttributeError:
            # happens for classes themselves
            klass = method = None
        else:
            method = getattr(klass, key, None)
        if not method:
            underKey = '_' + key
            method = klass and getattr(klass, underKey, None) or None
            if not method:
                attr = getattr(obj, key, NoDefault)
                if attr is NoDefault:
                    attr = getattr(obj, underKey, NoDefault)
                    if attr is NoDefault:
                        if klass is not None:
                            getitem = getattr(klass, '__getitem__', None)
                            if getitem:
                                try:
                                    getitem(obj, key)
                                except KeyError:
                                    unknown = True

    if not unknown:
        if method:
            return method(obj)
        if attr is not NoDefault:
            return attr

    if default is not NoDefault:
        return default

    raise ValueForKeyError(key)


def valueForName(obj, name, default=NoDefault):
    """Get the value of the object that is named.

    The name can use dotted notation to traverse through a network/graph
    of objects. Since this function relies on valueForKey() for each
    individual component of the name, you should be familiar with the
    semantics of that notation.

    Example: valueForName(obj, 'department.manager.salary')

    """
    for name in name.split('.'):
        obj = valueForKey(obj, name, default)
        if obj is default:
            break
    return obj