"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/dulcinea/lib/dumper.py $
$Id: dumper.py 22988 2003-11-05 01:56:52Z dbinger $

Dump Python data structures (including class instances) in a nicely-
nested, easy-to-read form.  Handles recursive data structures properly,
and has sensible options for limiting the extent of the dump both by
simple depth and by some rules for how to handle contained instances.
"""

import sys, string
from StringIO import StringIO
from types import *

DICT_TYPES = {DictionaryType: 1}
try:
    from BTree import BTree
    DICT_TYPES[BTree] = 1
except ImportError:
    pass
try:
    from BTrees import OOBTree, OIBTree, IOBTree
    DICT_TYPES[OOBTree.OOBTree] = 1
    DICT_TYPES[OIBTree.OIBTree] = 1
    DICT_TYPES[IOBTree.IOBTree] = 1
except ImportError:
    pass

#
# IDEAS on how to restrict how deep we go when dumping:
#   - never follow cyclic links! (handled with 'seen' hash)
#   - have a maximum dumping depth (for any nestable structure:
#     list/tuple, dictionary, instance)
#   - if in an instance: don't dump any other instances
#   - if in an instance: don't dump instances that belong to classes from
#     other packages
#   - if in an instance: don't dump instances that belong to classes from
#     other modules
# ...ok, these are all implemented now -- cool!

# restrictions on dumping
max_depth = 5
instance_dump = 'module'                # or 'all', 'none', 'package':
                                        # controls whether we dump an
                                        # instance that is under another
                                        # instance (however deep)

# arg -- this is necessary because the .__name__ of a type object
# under JPython is a bit ugly (eg. 'org.python.core.PyList' not 'list')
TYPE_NAMES = {
    BuiltinFunctionType: 'builtin',
    BuiltinMethodType: 'builtin',
    ClassType: 'class',
    CodeType: 'code',
    ComplexType: 'complex',
    DictType: 'dictionary',
    DictionaryType: 'dictionary',
    EllipsisType: 'ellipsis',
    FileType: 'file',
    FloatType: 'float',
    FrameType: 'frame',
    FunctionType: 'function',
    InstanceType: 'instance',
    IntType: 'int',
    LambdaType: 'function',
    ListType: 'list',
    LongType: 'long int',
    MethodType: 'instance method',
    ModuleType: 'module',
    NoneType: 'None',
    SliceType: 'slice',
    StringType: 'string',
    TracebackType: 'traceback',
    TupleType: 'tuple',
    TypeType: 'type',
    UnboundMethodType: 'instance method',
    UnicodeType: 'unicode',
    XRangeType: 'xrange',
    }

def get_type_name(type):
    try:
        return TYPE_NAMES[type]
    except KeyError:
        return type.__name__

def is_instance(val):
    if type(val) is InstanceType:
        return 1
    # instance of extension class, but not an actual extension class
    elif (hasattr(val, '__class__') and
          hasattr(val, '__dict__') and
          not hasattr(val, '__bases__')):
        return 1
    else:
        return 0

def is_class(val):
    return hasattr(val, '__bases__')

def atomic_type(t):
    return t in (NoneType, StringType, IntType, LongType, FloatType,
                 ComplexType)

def short_value(val):
    #if atomic_type(type(val)):
    #    return 1
    t = type(val)
    if (not DICT_TYPES.has_key(t) and t not in (ListType, TupleType) and
            not is_instance(val)):
        return 1
    elif t in (ListType, TupleType) and len(val) <= 10:
        for x in val:
            if not atomic_type(type(x)):
                return 0
        return 1
    elif DICT_TYPES.has_key(t) and len(val) <= 5:
        for (k,v) in val.items():
            if not (atomic_type(type(k)) and atomic_type(type(v))):
                return 0
        return 1
    else:
        return 0


def object_summary(val):
    t = type(val)
    if is_instance(val):
        if hasattr(val, '__str__'):
            strform = ": " + str(val)
        else:
            strform = ""
        return "<%s at %x%s>" % (val.__class__.__name__, id(val), strform)
    elif is_class(val):
        return "<%s %s at 0x%x>" % (get_type_name(t), val.__name__, id(val))
    else:
        return "<%s at 0x%x>" % (get_type_name(t), id(val))

def short_dump(val):
    if atomic_type(type(val)) or is_instance(val) or is_class(val):
        return `val`
    else:
        return object_summary(val) + ': ' + `val`

class Dumper:

    def __init__(self, max_depth=None, instance_dump=None, output=None):
        self._max_depth = max_depth
        self._instance_dump = instance_dump
        if output is None:
            self.out = sys.stdout
        else:
            self.out = output

    def __getattr__(self, attr):
        if self.__dict__.has_key('_' + attr):
            val = self.__dict__['_' + attr]
            if val is None:             # not defined in object;
                # attribute exists in instance (after adding _), but
                # not defined: get it from the module globals
                globals = vars(sys.modules[__name__])
                return globals.get(attr)
            else:
                # _ + attr exists and is defined (non-None)
                return val
        else:
            # _ + attr doesn't exist at all
            raise AttributeError, attr

    def __setattr__(self, attr, val):
        if self.__dict__.has_key('_' + attr):
            self.__dict__['_' + attr] = val
        else:
            self.__dict__[attr] = val

    def _writeln(self, line):
        self.out.write(line + "\n")

    def dump(self, val, indent='', summarize=1):
        self.seen = {}
        self.containing_instance = []
        self._dump(val, indent=indent, summarize=summarize)

    def _dump(self, val, depth=0, indent='', summarize=1):
        t = type(val)
        if short_value(val):
            self._writeln("%s%s" % (indent, short_dump(val)))
        else:
            depth = depth + 1
            if depth > self.max_depth:
                self._writeln(indent + "contents suppressed (too deep)")
                return
            if self.seen.get(id(val)):
                self._writeln(indent + "object already seen")
                return
            self.seen[id(val)] = 1
            if DICT_TYPES.has_key(t):
                if summarize:
                    self._writeln("%s%s:" % (indent, object_summary(val)))
                    indent = indent + '  '
                self.dump_dict(val, depth, indent)
            elif t in (ListType, TupleType):
                if summarize:
                    self._writeln("%s%s:" % (indent, object_summary(val)))
                    indent = indent + '  '
                self.dump_sequence(val, depth, indent)
            elif is_instance(val):
                self.dump_instance(val, depth, indent, summarize)
            else:
                raise RuntimeError, "this should not happen"

    def dump_dict(self, dict, depth, indent, shallow_attrs=()):
        keys = dict.keys()
        if type(keys) is ListType:
            keys.sort()
        for k in keys:
            val = dict[k]
            if short_value(val) or k in shallow_attrs:
                self._writeln("%s%s: %s" % (indent, k, short_dump(val)))
            else:
                self._writeln("%s%s: %s" % (indent, k, object_summary(val)))
                self._dump(val, depth, indent+'  ', summarize=0)

    def dump_sequence(self, seq, depth, indent):
        for i in range(len(seq)):
            val = seq[i]
            if short_value(val):
                self._writeln("%s%d: %s" % (indent, i, short_dump(val)))
            else:
                self._writeln("%s%d: %s" % (indent, i, object_summary(val)))
                self._dump(val, depth, indent+'  ', summarize=0)


    def dump_instance(self, inst, depth, indent, summarize=1):
        if summarize:
            self._writeln(indent + "%s " % object_summary(inst))
            indent = indent + '  '
        instance_dump = self.instance_dump
        # already dumping a containing instance, and have some restrictions
        # on instance-dumping?
        if self.containing_instance and instance_dump != 'all':
            previous_instance = self.containing_instance[-1]
            container_module = previous_instance.__class__.__module__
            container_package = (string.split(container_module, '.'))[0:-1]
            current_module = inst.__class__.__module__
            current_package = (string.split(current_module, '.'))[0:-1]
            # inhibit dumping of all contained instances?
            if instance_dump == 'none':
                return
            # inhibit dumping instances from a different package?
            elif (instance_dump == 'package' and
                  current_package != container_package):
                return
            # inhibit dumping instances from a different module?
            elif (instance_dump == 'module' and
                  current_module != container_module):
                return
        # if in containing instance and have restrictions
        self.containing_instance.append(inst)
        shallow_attrs = getattr(inst, "_dump_shallow_attrs", [])
        self.dump_dict(vars(inst), depth, indent, shallow_attrs)
        del self.containing_instance[-1]

default_dumper = Dumper()

def dump(val, output=None):
    if output is None:
        default_dumper.dump(val)
    else:
        Dumper(output=output).dump(val)

def dumps(val):
    out = StringIO()
    Dumper(output=out).dump(val)
    return out.getvalue()


if __name__ == "__main__":
    l1 = [3, 5, 'hello']
    t1 = ('uh', 'oh')
    l2 = ['foo', t1]
    d1 = {'k1': 'val1',
          'k2': l1,
          'k2': l2}

    dump(l1)
    dump(t1)
    dump(l2)
    dump(d1)

    dumper = Dumper(max_depth=1)
    l = ['foo', ['bar', 'baz', (1, 2, 3)]]
    dumper.dump(l)
    dumper.max_depth = 2
    dumper.dump(l)
    l[1][2] = tuple(range(11))
    dumper.dump(l)
    dumper.max_depth = None
    print dumper.max_depth

    class Foo: pass
    class Bar: pass

    f = Foo()
    b = Bar()

    f.a1 = 35
    f.a2 = l1
    f.a3 = l2
    f.b = b
    f.a4 = l2
    b.a1 = f
    b.a2 = None
    b.a3 = 'foo'
    dump(f)
