[Zope-Checkins] SVN: Zope/branches/jim-fix-zclasses/lib/python/ZODB/ Added ZClass-independent test of (and possible base class for)

Jim Fulton jim at zope.com
Mon Apr 4 07:04:28 EDT 2005


Log message for revision 29870:
  Added ZClass-independent test of (and possible base class for)
  persistent-class support machinery.
  

Changed:
  A   Zope/branches/jim-fix-zclasses/lib/python/ZODB/persistentclass.py
  A   Zope/branches/jim-fix-zclasses/lib/python/ZODB/persistentclass.txt
  A   Zope/branches/jim-fix-zclasses/lib/python/ZODB/tests/testpersistentclass.py

-=-
Added: Zope/branches/jim-fix-zclasses/lib/python/ZODB/persistentclass.py
===================================================================
--- Zope/branches/jim-fix-zclasses/lib/python/ZODB/persistentclass.py	2005-04-04 11:04:21 UTC (rev 29869)
+++ Zope/branches/jim-fix-zclasses/lib/python/ZODB/persistentclass.py	2005-04-04 11:04:27 UTC (rev 29870)
@@ -0,0 +1,224 @@
+##############################################################################
+#
+# Copyright (c) 2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (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 Class Support
+
+$Id$
+"""
+
+
+# Notes:
+# 
+# Persistent classes are non-ghostable.  This has some interesting
+# ramifications:
+# 
+# - When an object is invalidated, it must reload it's state
+# 
+# - When an object is loaded from the database, it's state must be
+#   loaded.  Unfortunately, there isn't a clear signal when an object is
+#   loaded from the database.  This should probably be fixed.
+# 
+#   In the mean time, we need to infer.  This should be viewed as a
+#   short term hack.
+# 
+#   Here's the strategy we'll use:
+# 
+#   - We'll have a need to be loaded flag that we'll set in
+#     __new__, through an extra argument.
+# 
+#   - When setting _p_oid and _p_jar, if both are set and we need to be
+#     loaded, then we'll load out state.
+# 
+#   - We'll use _p_changed is None to indicate that we're in this state.
+# 
+
+class _p_DataDescr(object):
+    # Descr used as base for _p_ data. Data are stored in
+    # _p_class_dict.
+
+    def __init__(self, name):
+        self.__name__ = name
+
+    def __get__(self, inst, cls):
+        if inst is None:
+            return self
+
+        if '__global_persistent_class_not_stored_in_DB__' in inst.__dict__:
+            raise AttributeError, self.__name__
+        return inst._p_class_dict.get(self.__name__)
+    
+    def __set__(self, inst, v):
+        inst._p_class_dict[self.__name__] = v
+
+    def __delete__(self, inst):
+        raise AttributeError, self.__name__
+
+class _p_oid_or_jar_Descr(_p_DataDescr):
+    # Special descr for _p_oid and _p_jar that loads
+    # state when set if both are set and and _p_changed is None
+    #
+    # See notes above
+    
+    def __set__(self, inst, v):
+        get = inst._p_class_dict.get
+        if v == get(self.__name__):
+            return
+        
+        inst._p_class_dict[self.__name__] = v
+        
+        jar = get('_p_jar')
+        if (jar is not None
+            and get('_p_oid') is not None
+            and get('_p_changed') is None
+            ):
+            jar.setstate(inst)
+
+class _p_ChangedDescr(object):
+    # descriptor to handle special weird emantics of _p_changed
+    
+    def __get__(self, inst, cls):
+        if inst is None:
+            return self
+        return inst._p_class_dict['_p_changed']
+        
+    def __set__(self, inst, v):
+        if v is None:
+            return
+        inst._p_class_dict['_p_changed'] = bool(v)
+
+    def __delete__(self, inst):
+        inst._p_invalidate()
+
+class _p_MethodDescr(object):
+    """Provide unassignable class attributes
+    """
+
+    def __init__(self, func):
+        self.func = func
+
+    def __get__(self, inst, cls):
+        if inst is None:
+            return cls
+        return self.func.__get__(inst, cls)
+
+    def __set__(self, inst, v):
+        raise AttributeError, self.__name__
+
+    def __delete__(self, inst):
+        raise AttributeError, self.__name__
+    
+
+special_class_descrs = '__dict__', '__weakref__'
+
+class PersistentMetaClass(type):
+
+    _p_jar = _p_oid_or_jar_Descr('_p_jar')
+    _p_oid = _p_oid_or_jar_Descr('_p_oid')
+    _p_changed = _p_ChangedDescr()
+    _p_serial = _p_DataDescr('_p_serial')
+
+    def __new__(self, name, bases, cdict, _p_changed=False):
+        cdict = dict([(k, v) for (k, v) in cdict.items()
+                      if not k.startswith('_p_')])
+        cdict['_p_class_dict'] = {'_p_changed': _p_changed}
+        return super(PersistentMetaClass, self).__new__(
+            self, name, bases, cdict)
+
+    def __getnewargs__(self):
+        return self.__name__, self.__bases__, {}, None
+
+    __getnewargs__ = _p_MethodDescr(__getnewargs__)
+
+    def _p_maybeupdate(self, name):
+        get = self._p_class_dict.get
+        data_manager = get('_p_jar')
+
+        if (
+            (data_manager is not None)
+            and          
+            (get('_p_oid') is not None)
+            and
+            (get('_p_changed') == False)
+            ):
+            
+            self._p_changed = True
+            data_manager.register(self)
+
+    def __setattr__(self, name, v):
+        if not ((name.startswith('_p_') or name.startswith('_v'))):
+            self._p_maybeupdate(name)
+        super(PersistentMetaClass, self).__setattr__(name, v)
+
+    def __delattr__(self, name):
+        if not ((name.startswith('_p_') or name.startswith('_v'))):
+            self._p_maybeupdate(name)
+        super(PersistentMetaClass, self).__delattr__(name)
+
+    def _p_deactivate(self):
+        # persistent classes can't be ghosts
+        pass
+
+    _p_deactivate = _p_MethodDescr(_p_deactivate)
+
+    def _p_invalidate(self):
+        # reset state
+        self._p_class_dict['_p_changed'] = None
+        self._p_jar.setstate(self)
+
+    _p_invalidate = _p_MethodDescr(_p_invalidate)
+
+
+    def __getstate__(self):
+        return (self.__bases__, 
+                dict([(k, v) for (k, v) in self.__dict__.items()
+                      if not (k.startswith('_p_')
+                              or k.startswith('_v_')
+                              or k in special_class_descrs
+                              )
+                     ]),
+                )
+                
+    __getstate__ = _p_MethodDescr(__getstate__)
+    
+    def __setstate__(self, state):
+        self.__bases__, cdict = state
+        cdict = dict([(k, v) for (k, v) in cdict.items()
+                      if not k.startswith('_p_')])
+
+        _p_class_dict = self._p_class_dict
+        self._p_class_dict = {}
+
+        to_remove = [k for k in self.__dict__
+                     if ((k not in cdict)
+                         and
+                         (k not in special_class_descrs)
+                         and
+                         (k != '_p_class_dict')
+                         )]
+
+        for k in to_remove:
+            delattr(self, k)
+        
+        for k, v in cdict.items():
+            setattr(self, k, v)
+
+        self._p_class_dict = _p_class_dict
+
+        self._p_changed = False
+        
+    __setstate__ = _p_MethodDescr(__setstate__)
+
+    def _p_activate(self):
+        self._p_jar.setstate(self)
+
+    _p_activate = _p_MethodDescr(_p_activate)


Property changes on: Zope/branches/jim-fix-zclasses/lib/python/ZODB/persistentclass.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: Zope/branches/jim-fix-zclasses/lib/python/ZODB/persistentclass.txt
===================================================================
--- Zope/branches/jim-fix-zclasses/lib/python/ZODB/persistentclass.txt	2005-04-04 11:04:21 UTC (rev 29869)
+++ Zope/branches/jim-fix-zclasses/lib/python/ZODB/persistentclass.txt	2005-04-04 11:04:27 UTC (rev 29870)
@@ -0,0 +1,286 @@
+Persistent Classes
+==================
+
+NOTE: persistent classes are EXPERIMENTAL and, in some sense,
+      incomplete.  This module exists largely to test changes made to
+      support Zope 2 ZClasses, with their historical flaws.
+
+The persistentclass module provides a meta class that can be used to implement
+persistent classes.
+
+Persistent classes have the following properties:
+
+- They cannot be turned into ghosts
+
+- They can only contain picklable subobjects
+
+- They don't live in regular file-system modules
+
+Let's look at an example:
+
+    >>> def __init__(self, name):
+    ...     self.name = name
+
+    >>> def foo(self):
+    ...     return self.name, self.kind
+
+    >>> import ZODB.persistentclass
+    >>> class C:
+    ...     __metaclass__ = ZODB.persistentclass.PersistentMetaClass
+    ...     __init__ = __init__
+    ...     __module__ = '__zodb__'
+    ...     foo = foo
+    ...     kind = 'sample'
+
+This example is obviously a bit contrived. In particular, we defined
+the methods outside of the class. Why?  Because all of the items in a
+persistent class must be picklable.  We defined the methods as global
+functions to make them picklable.
+
+Also note that we explictly set the module.  Persistent classes don't
+live in normal Python modules. Rather, they live in the database.  We
+use information in __module__ to record where in the database.  When
+we want to use a database, we will need to supply a custom class
+factory to load instances of the class.
+
+The class we created works a lot like other persistent objects.  It
+has standard standard persistent attributes:
+
+    >>> C._p_oid
+    >>> C._p_jar
+    >>> C._p_serial
+    >>> C._p_changed
+    False
+
+Because we haven't saved the object, the jar, oid, and serial are all
+None and it's not changed.
+
+We can create and use instances of the class:
+
+    >>> c = C('first')
+    >>> c.foo()
+    ('first', 'sample')
+
+We can modify the class and none of the persistent attributes will
+change because the object hasn't been saved.
+
+    >>> def bar(self):
+    ...     print 'bar', self.name
+    >>> C.bar = bar
+    >>> c.bar()
+    bar first
+
+    >>> C._p_oid
+    >>> C._p_jar
+    >>> C._p_serial
+    >>> C._p_changed
+    False
+
+Now, we can store the class in a database. We're going to use an
+explicit transaction manager so that we can show parallel transactions
+without having to use threads.
+
+    >>> import transaction
+    >>> tm = transaction.TransactionManager()
+    >>> connection = some_database.open(txn_mgr=tm)
+    >>> connection.root()['C'] = C
+    >>> tm.commit()
+
+Now, if we look at the persistence variables, we'll see that they have
+values:
+
+    >>> C._p_oid
+    '\x00\x00\x00\x00\x00\x00\x00\x01'
+    >>> C._p_jar is not None
+    True
+    >>> C._p_serial is not None
+    True
+    >>> C._p_changed
+    False
+
+Now, if we modify the class:
+
+    >>> def baz(self):
+    ...     print 'baz', self.name
+    >>> C.baz = baz
+    >>> c.baz()
+    baz first
+
+We'll see that the class has changed:
+
+    >>> C._p_changed
+    True
+
+If we abort the transaction:
+
+    >>> tm.abort()
+
+Then the class will return to it's prior state:
+
+    >>> c.baz()
+    Traceback (most recent call last):
+    ...
+    AttributeError: 'C' object has no attribute 'baz'
+
+    >>> c.bar()
+    bar first
+
+We can open another connection and access the class there.
+
+    >>> tm2 = transaction.TransactionManager()
+    >>> connection2 = some_database.open(txn_mgr=tm2)
+
+    >>> C2 = connection2.root()['C']
+    >>> c2 = C2('other')
+    >>> c2.bar()
+    bar other
+
+If we make changes without commiting them:
+
+    >>> C.bar = baz
+    >>> c.bar()
+    baz first
+
+    >>> C is C2
+    False
+
+Other connections are unaffected:
+
+    >>> connection2.sync()
+    >>> c2.bar()
+    bar other
+
+Until we commit:
+
+    >>> tm.commit()
+    >>> connection2.sync()
+    >>> c2.bar()
+    baz other
+
+Similarly, we don't see changes made in other connections:
+
+    >>> C2.color = 'red'
+    >>> tm2.commit()
+
+    >>> c.color
+    Traceback (most recent call last):
+    ...
+    AttributeError: 'C' object has no attribute 'color'
+
+until we sync:
+
+    >>> connection.sync()
+    >>> c.color
+    'red'
+
+Instances of Persistent Classes
+-------------------------------
+
+We can, of course, store instances of perstent classes in the
+database:
+
+    >>> c.color = 'blue'
+    >>> connection.root()['c'] = c
+    >>> tm.commit()
+
+    >>> connection2.sync()
+    >>> connection2.root()['c'].color
+    'blue'
+
+NOTE: If a non-persistent instance of a persistent class is copied,
+      the class may be copied as well. This is usually not the desired
+      result. 
+
+
+Persistent instances of persistent classes
+------------------------------------------
+
+Persistent instances of persistent classes are handled differently
+than normal instances.  When we copy a persistent instances of a
+persistent class, we want to avoid copying the class.
+
+Lets create a persistent class that subclasses Persistent:
+
+    >>> import persistent
+    >>> class P(persistent.Persistent, C):
+    ...     __module__ = '__zodb__'
+    ...     color = 'green'
+
+    >>> connection.root()['P'] = P
+
+    >>> import persistent.mapping
+    >>> connection.root()['obs'] = persistent.mapping.PersistentMapping()
+    >>> p = P('p')
+    >>> connection.root()['obs']['p'] = p
+    >>> tm.commit()
+
+You might be wondering why we didn't just stick 'p' into the root
+object. We created an intermediate persistent object instead.  We are
+storing persistent classes in the root object. To create a ghost for a
+persistent instance of a persistent class, we need to be able to be
+able to access the root object and it must be loaded first.  If the
+instance was in the root object, we'd be unable to create it while
+loading the root object.
+
+Now, if we try to load it, we get a broken oject:
+
+    >>> connection2.sync()
+    >>> connection2.root()['obs']['p']
+    <persistent broken __zodb__.P instance '\x00\x00\x00\x00\x00\x00\x00\x04'>
+
+because the module, "__zodb__" can't be loaded.  We need to provide a
+class factory that knows about this special module. Here we'll supply a
+sample class factory that looks up a class name in the database root
+if the module is "__zodb__".  It falls back to the normal class lookup 
+for other modules:
+
+    >>> from ZODB.broken import find_global
+    >>> def classFactory(connection, modulename, globalname):
+    ...     if modulename == '__zodb__':
+    ...        return connection.root()[globalname]
+    ...     return find_global(modulename, globalname)
+
+    >>> some_database.classFactory = classFactory
+
+Normally, the classFactory should be set before a database is opened. 
+We'll reopen the connections we're using.  We'll assign the old
+connections to a variable first to prevent getting them from the
+connection pool:
+
+    >>> old = connection, connection2
+    >>> connection = some_database.open(txn_mgr=tm)
+    >>> connection2 = some_database.open(txn_mgr=tm2)
+   
+Now, we can read the object:
+
+    >>> connection2.root()['obs']['p'].color
+    'green'
+    >>> connection2.root()['obs']['p'].color = 'blue'
+    >>> tm2.commit()
+
+    >>> connection.sync()
+    >>> p = connection.root()['obs']['p']
+    >>> p.color
+    'blue'
+
+Copying
+-------
+
+If we copy an instance via export/import, the copy and the original
+share the same class:
+
+    >>> file = connection.exportFile(p._p_oid)
+    >>> file.seek(0)
+    >>> cp = connection.importFile(file)
+    >>> cp.color
+    'blue'
+
+    >>> cp is not p
+    True
+
+    >>> cp.__class__ is p.__class__
+    True
+
+
+
+XXX test abort of import


Property changes on: Zope/branches/jim-fix-zclasses/lib/python/ZODB/persistentclass.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: Zope/branches/jim-fix-zclasses/lib/python/ZODB/tests/testpersistentclass.py
===================================================================
--- Zope/branches/jim-fix-zclasses/lib/python/ZODB/tests/testpersistentclass.py	2005-04-04 11:04:21 UTC (rev 29869)
+++ Zope/branches/jim-fix-zclasses/lib/python/ZODB/tests/testpersistentclass.py	2005-04-04 11:04:27 UTC (rev 29870)
@@ -0,0 +1,51 @@
+##############################################################################
+#
+# Copyright (c) 2004 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.
+#
+##############################################################################
+"""ZClass tests
+
+$Id$
+"""
+
+import os, sys
+import unittest
+import ZODB.tests.util
+import transaction
+from zope.testing import doctest
+
+
+# XXX need to update files to get newer testing package
+class FakeModule:
+    def __init__(self, name, dict):
+        self.__dict__ = dict
+        self.__name__ = name
+
+
+def setUp(test):
+    test.globs['some_database'] = ZODB.tests.util.DB()
+    module = FakeModule('ZClasses.example', test.globs)
+    sys.modules[module.__name__] = module
+    
+def tearDown(test):
+    transaction.abort()
+    test.globs['some_database'].close()
+    del sys.modules['ZClasses.example']
+
+def test_suite():
+    return unittest.TestSuite((
+        doctest.DocFileSuite("../persistentclass.txt",
+                             setUp=setUp, tearDown=tearDown),
+        ))
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='test_suite')
+


Property changes on: Zope/branches/jim-fix-zclasses/lib/python/ZODB/tests/testpersistentclass.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native



More information about the Zope-Checkins mailing list