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 _______________________________________________ Zope-Checkins maillist - Zope-Checkins@zope.org http://mail.zope.org/mailman/listinfo/zope-checkins