[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