############################################################################## # # Copyright (c) 2002 Zope Corporation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """Persistent Classes.""" __metaclass__ = type from zope.interface import implements from persistent.cPersistence import UPTODATE, CHANGED, GHOST from persistent.interfaces import IPersistent from zodbcode.function import PersistentFunction import time # XXX There is a lot of magic here to give classes and instances # separate sets of attributes. This code should be documented, as it # it quite delicate, and it should be move to a separate module. class SimpleDescriptor(object): missing = object() def __init__(self, value): self._value = value def __get__(self, obj, cls): if self._value is self.missing: raise AttributeError return self._value def __set__(self, obj, value): self._value = value def __delete__(self, obj): if self._value is self.missing: raise AttributeError del self._value class ExtClassDescr: """Maintains seperate class and instance descriptors for an attribute. This allows a class to provide methods and attributes without intefering with normal use of instances. The class and its instances can each have methods with the same name. This does interfere with introspection on the class. """ def __init__(self, name, instdescr): self.name = name self.instdescr = instdescr def __get__(self, obj, cls): if obj is None: return self.clsget(cls) else: return self.instdescr.__get__(obj, cls) def __set__(self, obj, val): if obj is None: self.clsset(val) else: if self.instdescr is None: raise AttributeError(self.name) return self.instdescr.__set__(obj, val) def __delete__(self, obj): if self.instdescr is None: raise AttributeError(self.name) return self.instdescr.__delete__(obj) # subclass should override def clsget(self, cls): pass def clsset(self, val): pass def clsdelete(self): pass class MethodMixin: def __init__(self, name, descr, func): if not hasattr(descr, "__get__"): # If the object defined in the metaclass is not a descriptor, # create one for it. descr = SimpleDescriptor(descr) super(MethodMixin, self).__init__(name, descr) self.func = func def clsget(self, cls): def f(*args, **kwargs): try: return self.func(cls, *args, **kwargs) except TypeError: print `self.func`, `cls`, `args`, `kwargs` raise return f class DataMixin: def __init__(self, name, descr, val): if not hasattr(descr, "__get__"): # If the object defined in the metaclass is not a descriptor, # create one for it. descr = SimpleDescriptor(descr) super(DataMixin, self).__init__(name, descr) self.val = val def clsget(self, cls): return self.val def clsset(self, val): self.val = val def clsdelete(self): del self.val class ExtClassMethodDescr(MethodMixin, ExtClassDescr): pass class ExtClassDataDescr(DataMixin, ExtClassDescr): pass class ExtClassHookDataDescr(ExtClassDataDescr): # Calls a hook when clsset() is called. def __init__(self, name, descr, val, hook): super(ExtClassHookDataDescr, self).__init__(name, descr, val) self.hook = hook def clsset(self, val): self.val = val self.hook() # The next three classes conspire to make a PersistentFunction # behave like a method when found in a class's __dict__. class PersistentMethod: """Make PersistentFunctions into methods.""" def __init__(self, klass, inst, func): self.im_class = klass self.im_self = inst self.im_func = func def __repr__(self): if self.im_self is None: fmt = "" else: fmt = "" % (self.im_self,) return fmt % (self.im_class.__name__, self.im_func.__name__) def __call__(self, *args, **kwargs): if self.im_self is None: if not isinstance(args[0], self.im_class): raise TypeError("unbound method %s() must be called " "with %s instance as first argument (" "got %s instead)" % (self.im_func.__name__, self.im_class.__name__, type(args[0]).__name__)) else: return self.im_func(self.im_self, *args, **kwargs) class PersistentDescriptor: def __init__(self, objclass, func): self.__name__ = func.__name__ self.__doc__ = func.__doc__ self.__objclass__ = objclass self._func = func # Delegate __getstate__ and __setstate__ to the persistent func. # The patch module will use these methods to update persistent # methods in place. self.__getstate__ = func.__getstate__ self.__setstate__ = func.__setstate__ def __repr__(self): return "" % (self.__objclass__.__name__, self.__name__) def __get__(self, object, klass=None): if object is None: return PersistentMethod(klass or self.__objclass__, None, self._func) else: return PersistentMethod(klass or self.__objclass__, object, self._func) _missing = object() def findattr(cls, attr, default): """Walk the mro of cls to find attr.""" for c in cls.__mro__: o = c.__dict__.get(attr, _missing) if o is not _missing: return o return default class StateChangeDataDescr(ExtClassDataDescr): # A data descriptor for _p_changed. pass class PersistentClassMetaClass(type): # An attempt to make persistent classes look just like other # persistent objects by providing class attributes and methods # that behave like the persistence machinery. # The chief limitation of this approach is that class.attr won't # always behave the way it does for normal classes # A persistent class can never be a ghost, because there are too # many places where Python will attempt to inspect the class # without using getattr(). As a result, it would be impossible to # guarantee that the class would be unghostified at the right # time. It's really difficult to guarantee this property without # help from the connection, because a ghost can't be unghosted # until after the connection sets its _p_jar. # The hack solution is to have a hook for _p_jar that activates # the object the first time it is set. #implements(IPersistent) __implements__ = IPersistent # A class is normally created in the UPTODATE state, but when a # new ghost is created for it the serialization machinery passes # GHOST instead of UPTODATE. See __getnewargs__(). def __new__(meta, name, bases, dict, state=UPTODATE): if "__dict__" in dict: del dict["__dict__"] cls = super(PersistentClassMetaClass, meta).__new__( meta, name, bases, dict) cls._pc_init = False # helper functions def extend_attr(attr, v): prev = findattr(cls, attr, SimpleDescriptor.missing) setattr(cls, attr, ExtClassDataDescr(attr, prev, v)) def extend_meth(attr, m): prev = findattr(cls, attr, SimpleDescriptor.missing) setattr(cls, attr, ExtClassMethodDescr(attr, prev, m)) extend_attr("_p_oid", None) extend_attr("_p_atime", time.time() % 86400) extend_attr("_p_state", state) extend_attr("_p_changed", None) extend_meth("_p_activate", meta._p_activate) extend_meth("_p_deactivate", meta._p_deactivate) # XXX _p_invalidate # Create a descriptor that calls _p_activate() when _p_jar is set. inst_jar_descr = findattr(cls, "_p_jar", None) setattr(cls, "_p_jar", ExtClassHookDataDescr("_p_jar", inst_jar_descr, None, getattr(cls, "_p_activate"))) for k, v in dict.items(): if isinstance(v, PersistentFunction): setattr(cls, k, PersistentDescriptor(cls, v)) # A class could define any of these attributes, thus we # need to create extended descriptors so that the class # and its instances have separate versions. extend_meth("__getstate__", meta.__getstate__) extend_meth("__setstate__", meta.__setstate__) # Don't need this with interface geddon # extend_attr("__implements__", meta.__implements__) cls._pc_init = True return cls def __getattribute__(cls, name): # XXX I'm not sure I understand this code any more. super_meth = super(PersistentClassMetaClass, cls).__getattribute__ # If we are initializing the class, don't trying to check variables # like _p_state, since they may not be initialized. if not super_meth("_pc_init"): return super_meth(name) if (name[0] != "_" or not (name.startswith("_p_") or name.startswith("_pc_") or name == "__dict__")): if cls._p_state == GHOST: cls._p_activate() cls._p_atime = int(time.time() % 86400) return super_meth(name) # XXX There needs to be an _p_changed flag so that classes get # registered with the txn when they are modified. def __setattr__(cls, attr, val): if not attr.startswith("_pc_") and cls._pc_init: descr = cls.__dict__.get(attr) if descr is not None: set = getattr(descr, "__set__", None) if set is not None: set(None, val) ## cls._p_changed = True return super(PersistentClassMetaClass, cls).__setattr__(attr, val) def __delattr__(cls, attr): if attr.startswith('_p_'): # XXX what should happen with these? return super(PersistentClassMetaClass, cls).__delattr__(attr) def __repr__(cls): return "" % (cls.__module__, cls.__name__) # It should be possible for getstate / setstate to deal with # arbitrary class attributes. That goal is hard to achieve, # because there are several funny descriptors that need to # be handled specially. def __getstate__(cls): dict = {} for k in cls.__dict__.keys(): v = getattr(cls, k) if isinstance(v, PersistentMethod): dict[k] = v.im_func continue if (k in ["__module__", "__weakref__", "__dict__"] or k.startswith("_p_") or k.startswith("_pc_")): continue # XXX The following test isn't right because overriding # must be allowed, but I haven't figured that out yet. # __getstate__ and __setstate__ might be overridden # __implements__ might be overridden if k in ["__getstate__", "__setstate__", "__implements__"]: continue dict[k] = v return dict def __setstate__(cls, dict): for k, v in dict.items(): if isinstance(v, PersistentFunction): setattr(cls, k, PersistentDescriptor(cls, v)) else: setattr(cls, k, v) # XXX Should the object get marked as a ghost when it is, in fact, # not a ghost? The most obvious answer is no. But if we don't # then we need some other attribute that can be used to handle # invalidations of classes and make _p_activate() work as expected. # Need to decide on a good answer. def _p_deactivate(cls): # do nothing but mark the state change for now cls._p_state = GHOST def _p_activate(cls): # The logic here is: # If the class hasn't finished executing __new__(), don't # try to load its state. # If the class has a jar but no oid, it's a new object # and doesn't have state in the database. if cls._p_state == GHOST and cls._pc_init: dm = cls._p_jar if dm is not None and cls._p_oid: cls._p_state = CHANGED try: # XXX Make sure the object is in the cache before # calling setstate(). dm._cache[cls._p_oid] = cls dm.setstate(cls) finally: # XXX Should really put in special inconsistent state cls._p_state = UPTODATE else: print id(cls), "dm", dm, "oid", cls._p_oid # Methods below here are not wrapped to be class-only attributes. # They are available as methods of classes using this metaclass. def __getnewargs__(cls): # XXX This should really be _p_getnewargs() or something like that. # If the class is later loaded and unghostified, the arguments # passed to __new__() won't have an __module__. It seems that # the module gets set to zodb.code.class_ in that case, which # is wrong. return (cls.__name__, cls.__bases__, {"__module__": cls.__module__}, GHOST) def _p_newstate(cls, acls): # Update a class's __dict__ in place. Must use setattr and # delattr because __dict__ is a read-only proxy. # XXX This doesn't handle __methods__ correctly. # XXX I'm not sure how this is supposed to handle the # ExtClassDataDescrs. As a hack, I'm deleting _p_oid # and _p_jar from the keys dict, because I know they # will be descrs and they won't change as a result of # update. It appears that if the new class has a descr # that isn't set on the class, it will stomp on the old # class's value. Not sure if this is a problem in general. def getkeys(cls): L = [n for n in cls.__dict__.keys() if (not (n.startswith("__") and n.endswith("__")) and not n.startswith("_p_")) ] d = {} for elt in L: d[elt] = True return d oldnames = getkeys(cls) newnames = getkeys(acls) for name in oldnames: if not name in newnames: delattr(cls, name) for name in newnames: setattr(cls, name, acls.__dict__[name])