"""$URL: svn://svn/repos/trunk/dulcinea/lib/base.py $
$Id: base.py 20912 2003-03-04 16:07:12Z dbinger $

Provides universal base classes which all classes in the Dulcinea
library should subclass.  Currently there are two such
universal base classes:
    DulcineaPersistent    for classes whose instances should always be
                          first-class, persistent database objects
    DulcineaBase          for classes whose instances may be transient,
                          but can still be persisted if they happen to
                          be attached to another persistent object

"""
from __future__ import nested_scopes

import sys
import new
from types import ClassType
from ZODB import DB, utils
from ExtensionClass import ExtensionClass
from Persistence import Persistent


# If you're wondering which of DulcineaBase and DulcineaPersistent to use
# for some class, then use DulcineaPersistent: that will make instances of
# that class "first-class" database objects, subject to all of ZODB's
# machinery.  Usually this is a good thing, but keep in mind that deriving
# from DulcineaPersistent means your class is an ExtensionClass, which has
# a number of ramifications:
#   * the '__radd__()' (and, presumably, '__rmul__()', '__rsub__()',
#     '__rdiv__()') protocol doesn't work: this is a bug in
#     ExtensionClass.
#   * the '__cmp__()' protocol may not work as expected (unsure on
#     the details here)
#
# In short, be leery of ExtensionClass (and thus DulcineaPersistent) for
# any class that needs to do operator overloading: this stuff is rather
# flaky for ExtensionClasses.  Conversely, only do operator overloading if
# it's really necessary and a big win, since it means that objects of
# classes with operator overloads won't be able to be first-class database
# objects, at least until the '__radd__()' bug in ExtensionClass is fixed.


class DulcineaBase:
    """The standard base class for classes whose instances do not need
    to be first-class database objects.  Instances of DulcineaBase can still
    be stored in the database if they happen to be attached to
    other persistent objects, but they don't really participate in
    persistence -- they just come along for the ride.

    Instance attributes: none

    """

    # Our standard __repr__(): like Python's, except it also puts the
    # str() of the object in the repr() and uses bare class name
    # (no full module path).
    def __repr__ (self):
        if hasattr(self, '__str__'):
            strvalue = ": " + str(self)
        else:
            strvalue = ""
            
        return ("<%s at %08x%s>" % (self.__class__.__name__,
                                   id(self), strvalue))

    def _dump (self, file=None, header=1, deep=0):
        """dump(file : file = sys.stdout,
                header : boolean = true,
                deep : boolean = true)
           -> [string]

        Default low-level dump method.  'header' is always considered
        true, no matter what you pass in.  If 'deep' is true, you will
        get an appallingly deep, detailed dump!
        """
        from dulcinea.dumper import Dumper
        dumper = Dumper(instance_dump=(deep and "all" or "module"),
                        max_depth=None,
                        output=file)
        dumper.dump(self, summarize=header)

    # Subclass will normally override 'dump()', leaving '_dump()' still
    # available for when you need a gorily detailed dump.
    dump = _dump

    def dumps (self, header=1, deep=1):
        from cStringIO import StringIO
        f = StringIO()
        self.dump(file=f, header=header, deep=deep)
        return f.getvalue()

    def _startdump (self, file, header, text=None, extra=None):
        """_startdump(file : file | IndentedFile | None,
                      header : boolean,
                      text : string = None,
                      extra : string = None)
           -> IndentedFile

        Convenience method for writing 'dump()' methods in subclasses.
        Returns an IndentedFile object that you can just start writing
        lines of the dump to; writing the header line and setting up the
        indentation is already taken care of as follows: if 'header' is
        true, writes a header line consisting of 'text' and 'extra'
        separated by a space.  'text' defaults to the 'repr()' of the
        object; 'extra' defaults to nothing.
        """
        from dulcinea.indented_file import IndentedFile
        if file is None:
            file = sys.stdout
        if not isinstance(file, IndentedFile):
            file = IndentedFile(file, 0, 2)

        if header:
            if text is None:
                text = `self`
            if extra:
                text = text + ' ' + extra
            file.write(text + '\n')
            file.indent_more()

        return file

    def _dump_attrs (self, file, *attrs):
        """_dump_attrs(file, ...)

        Dump a list of attributes of the current object.  Each
        attribute in the argument list is printed on a line like
        "attr_name: attr_value", where 'attr_value' is the repr()
        of the attribute.
        """        
        for attr in attrs:
            val = getattr(self, attr)
            #if val is None: continue
            file.write("%s: %s\n" % (attr, `val`))

    def _dump_bool (self, file, attr):
        val = getattr(self, attr)
        file.write("%s: %s\n" % (attr, val and "true" or "false"))

    def _subdump (self, file, deep=1, attr=None, value=None, head=None):
        """Dump a sub-object of the current object.  Takes care of
        indenting and dedenting around the sub-dump.  'deep' determines
        whether we simply print a header line or really dump the
        sub-object, and it is passed to the sub-object's 'dump()' method
        as well.

        Normally, just supply 'attr', the name of the instance attribute
        to dump.  If 'attr' isn't supplied, you must supply 'head' and
        should supply 'value'.  'head' is the line printed immediately
        before dumping 'value'; it defaults to
          attr + ": " + repr(value)

        'value' is the object to dump; it defaults to the attribute
        value named by 'attr'.  'value' must have a 'dump()' method or
        be None.
        """
        if head is None and attr is None:
            raise RuntimeError, "must supply either 'head' or 'attr'"
        if value is None and attr is not None:
            value = getattr(self, attr)
        if head is None:
            head = "%s: %s" % (attr, `value`)

        file.write(head + "\n")
        if deep and value is not None:
            file.indent_more()
            value.dump(file, header=0, deep=deep)
            file.indent_less()

    def _dump_seq (self, file, deep=1, attr=None, value=None, head=None):
        # ugh, copied from above
        if head is None and attr is None:
            raise RuntimeError, "must supply either 'head' or 'attr'"
        if value is None and attr is not None:
            value = getattr(self, attr)
        if head is None:
            if value is None:
                head = "%s: None" % attr
            elif len(value) == 0:
                head = "%s: empty list" % attr
            else:
                head = "%s: %d elements" % (attr, len(value))

        file.write(head + "\n")
        if deep and value:
            file.indent_more()
            for i in range(len(value)):
                file.write("%d: %s\n" % (i, `value[i]`))
            file.indent_less()

    def copy (self):
        klass = self.__class__
        if type(klass) is ExtensionClass:
            newself = klass.__basicnew__()
        elif type(klass) is ClassType:
            newself = new.instance(self.__class__, {})
        elif isinstance(klass, object):
            newself = klass.__new__(klass)
        else:
            raise RuntimeError, "unknown type of class object: %s" % `klass`

        for attr in self.__dict__.keys():
            if attr[0:3] not in ("_p_", "_v_"):
                val = getattr(self, attr)
                setattr(newself, attr, val)

        return newself

    def __copy__ (self):
        return self.copy()

# class DulcineaBase


class DulcineaPersistent (DulcineaBase, Persistent):
    """The standard base class for classes whose instances should be
    first-class database objects that participate fully in the
    persistence mechanism.  Instances of DulcineaPersistent still have to be
    referenced by an existing persistent object, i.e. they must
    ultimately connect back to one of the database root objects.

    Instance attributes: none
    """

    # used below by __repr__ to determine if __str__ has been overridden
    _default__str__ = getattr(Persistent, "__str__", None)

    def __repr__ (self):
        str_method = getattr(self.__class__, '__str__', None)
        if str_method is not self.__class__._default__str__:
            # subclass overrode __str__ method, it's safe to call str()
            strvalue = ": " + str(self)
        else:
            # default __str__ falls back to __repr__ so don't call it
            strvalue = ""
        if self._p_oid:                 # brand-new objects don't have OID yet
            oid = "%016x" % utils.U64(self._p_oid)
        else:
            oid = "%08x" % id(self)
        return "<%s at %s%s>" % (self.__class__.__name__, oid, strvalue)

    def __hash__ (self):
        # 'object' provides a default hash method but ExtensionClass does not
        return id(self)

#
# Utility functions
#

try:
    from BTrees import OOBTree
    def _new_btree ():
        return OOBTree.OOBTree()
except ImportError:
    def _new_btree ():
        return {}

try:
    from Persistence import PersistentMapping
    def persistent_map_copy(self):
        return self.__class__(self.data)
    PersistentMapping.__copy__ = persistent_map_copy
    PersistentMapping.copy = persistent_map_copy
    def _new_map ():
        return PersistentMapping()

    # XXX ZODB4 drops support for old _container attribute.  We should
    # re-commit all PersistentMapping objects and remove this code.
    def persistent_map_setstate(self, dict):
        self.__dict__.update(dict)
        if hasattr(self, '_container'):
            self.data = self._container
            del self._container
    PersistentMapping.__setstate__ = persistent_map_setstate

except ImportError:
    def _new_map ():
        return {}

def new_persistent_map (big=0):
    """Return a new ZODB-aware dictionary-like object that's scaled for
    big or small numbers of keys, depending on the 'big' parameter.
    """
    # currently the big option doesn't do anything
    return _new_map()


def new_persistent_list (list=None):
    from dulcinea.persistent_list import PersistentList
    return PersistentList(list)

