"""Introspective functions for Python objects"""

def docstrings():
  if py_version < '1.5.2': return
  """
  containers.__doc__   = "A tuple of built-in types that contain parts"
  simpletypes.__doc__  = "A tuple of built-in types that have no parts"
  datatypes.__doc__    = "A tuple of all the built-in types that hold data"
  immutabletypes.__doc__="A tuple of built-in types that are immutable"
  """
  isContainer.__doc__  = "this and that"

import sys, string
# this is used throughout Gnosis to enable
# version-specific features/hacks
py_version = string.split(sys.version)[0]

from types import *
from operator import add
from gnosis.util.combinators import or_, not_, and_, lazy_any

containers = (ListType, TupleType, DictType)
simpletypes = (IntType, LongType, FloatType, ComplexType, StringType)
if py_version >= '2.0':
    simpletypes = simpletypes + (UnicodeType,)
datatypes = simpletypes+containers
immutabletypes = simpletypes+(TupleType,)

class undef: pass

def isinstance_any(o, types):
    "A varargs form of isinstance()"
    for t in types:
        if isinstance(o, t): return t

isContainer  = lambda o: isinstance_any(o, containers)
isSimpleType = lambda o: isinstance_any(o, simpletypes)
isInstance   = lambda o: type(o) is InstanceType
isImmutable  = lambda o: isinstance_any(o, immutabletypes)

if py_version >= '2.2':
    isNewStyleInstance = lambda o: issubclass(o.__class__,object) and \
                                not type(o) in datatypes
else:
    isNewStyleInstance = lambda o: 0
isOldStyleInstance = lambda o: isinstance(o, ClassType)
isClass         = or_(isOldStyleInstance, isNewStyleInstance)

def isNewStyleClass(o):
    try:
        return issubclass(o,object)
    except TypeError:
        return 0

hasSlots     = lambda o: hasattr(o,'__slots__')
hasInit      = lambda o: hasattr(o,'__init__')
hasDictAttrs = lambda o: (hasattr(o,'__dict__') and o.__dict__)

isInstanceLike = lazy_any(isInstance, hasDictAttrs, hasSlots)
hasCompoundShape = and_(isInstanceLike, not_(isInstance))

true_container = lambda o: type(o) in containers
true_simpletype = lambda o: type(o) in simpletypes
true_datatype = or_(true_container, true_simpletype)
child_container = and_(not_(true_container), isContainer)
child_simpletype = and_(not_(true_simpletype), isSimpleType)
child_datatype = or_(child_container, child_simpletype)

def hasCoreData(o):
    """Is 'o' an object subclassed from a builtin type?

    (i.e. does it contain data other than attributes)
    We only want subclasses, not the class itself (otherwise we'd
    catch ALL lists, integers, etc.)
    """
    return child_datatype(o)

# semantic convenience - maybe it'll do something else later?
wantsCoreData = hasCoreData

def attr_dict(o, fillslots=0):
    if hasattr(o,'__dict__'):
        return o.__dict__
    elif hasattr(o,'__slots__'):
        dct = {}
        for attr in o.__slots__:
            if fillslots and not hasattr(o, attr):
                setattr(o, attr, undef())
                dct[attr] = getattr(o,attr)
            elif hasattr(o, attr):
                dct[attr] = getattr(o,attr)
        return dct
    else:
        raise TypeError, "Object has neither __dict__ nor __slots__"

attr_keys = lambda o: attr_dict(o).keys()
attr_vals = lambda o: attr_dict(o).values()

def attr_update(o,new):
    for k,v in new.items():
        setattr(o,k,v)

def data2attr(o):
    "COPY (not move) data to __coredata__ 'magic' attribute"
    o.__coredata__ = getCoreData(o)
    return o

def attr2data(o):
    "Move 'magic' attribute back to 'core' data"
    if hasattr(o,'__coredata__'):
        o = setCoreData(o, o.__coredata__)
        del o.__coredata__
    return o

def setCoreData(o, data, force=0):
    "Set core data of obj subclassed from a builtin type, return obj"
    #-- Only newobjects unless force'd
    if not force and not wantsCoreData(o): pass
    #-- Tuple and simpletypes are immutable (need new copy)!
    elif isImmutable(o):
        # Python appears to guarantee that all classes subclassed
        # from immutables must take one and only one argument
        # to __init__ [calling __init__() is not usually allowed
        # when unpickling] ... lucky us :-)
        new = o.__class__(data)
        attr_update(new, attr_dict(o))  # __slots__ safe attr_dict()
        o = new
    elif isinstance(o, DictType):
        o.clear()
        o.update(data)
    elif isinstance(o, ListType):
        o[:] = data
    return o

def getCoreData(o):
    "Return the core data of object subclassed from builtin type"
    if hasCoreData(o):
        return isinstance_any(o, datatypes)(o)
    else:
        raise TypeError, "Unhandled type in getCoreData for: ", o

def instance_noinit(C):
    """Create an instance of class C without calling __init__

    [Note: This function was greatly simplified in gnosis-1.0.7,
    but I'll leave these notes here for future reference.]

    We go to some lengths here to avoid calling __init__.  It
    gets complicated because we cannot always do the same thing;
    it depends on whether there are __slots__, and even whether
    __init__ is defined in a class or in its parent.  Even still,
    there are ways to construct *highly artificial* cases where
    the wrong thing happens.

    If you change this, make sure ALL test cases still work ...
    easy to break things
    """
    if isOldStyleInstance(C):
        import new
        # 2nd arg is required for Python 2.0 (optional for 2.1+)
        return new.instance(C,{})
    elif isNewStyleInstance(C):
        return C.__new__(C)
    else:
        raise TypeError, "You must specify a class to create instance of."

if __name__ == '__main__':
    "We could use some could self-tests (see test/ subdir though)"
else:
    docstrings()

