[Zope-Checkins]
SVN: Zope/branches/jim-fix-zclasses/lib/python/ZClasses/
Created a meta class for persistent classes. This is based very
Jim Fulton
jim at zope.com
Mon Feb 7 07:36:01 EST 2005
Log message for revision 29069:
Created a meta class for persistent classes. This is based very
loosly on the experimental persistent class code from Zope 3.
Changed:
A Zope/branches/jim-fix-zclasses/lib/python/ZClasses/_pmc.py
A Zope/branches/jim-fix-zclasses/lib/python/ZClasses/_pmc.txt
A Zope/branches/jim-fix-zclasses/lib/python/ZClasses/tests.py
-=-
Added: Zope/branches/jim-fix-zclasses/lib/python/ZClasses/_pmc.py
===================================================================
--- Zope/branches/jim-fix-zclasses/lib/python/ZClasses/_pmc.py 2005-02-07 12:35:58 UTC (rev 29068)
+++ Zope/branches/jim-fix-zclasses/lib/python/ZClasses/_pmc.py 2005-02-07 12:36:01 UTC (rev 29069)
@@ -0,0 +1,227 @@
+##############################################################################
+#
+# 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.
+#
+##############################################################################
+"""ZCLass Persistent Meta Class
+
+IMPORTANT -- This module is private to ZClasses and experimetal.
+ It is highly subject to change and likely to move
+
+$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.
+#
+
+import ExtensionClass
+
+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
+ 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 ZClassPersistentMetaClass(ExtensionClass.ExtensionClass):
+
+ _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(ZClassPersistentMetaClass, 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(ZClassPersistentMetaClass, self).__setattr__(name, v)
+
+ def __delattr__(self, name):
+ if not ((name.startswith('_p_') or name.startswith('_v'))):
+ self._p_maybeupdate(name)
+ super(ZClassPersistentMetaClass, 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/ZClasses/_pmc.py
___________________________________________________________________
Name: svn:keywords
+ Id
Name: svn:eol-style
+ native
Added: Zope/branches/jim-fix-zclasses/lib/python/ZClasses/_pmc.txt
===================================================================
--- Zope/branches/jim-fix-zclasses/lib/python/ZClasses/_pmc.txt 2005-02-07 12:35:58 UTC (rev 29068)
+++ Zope/branches/jim-fix-zclasses/lib/python/ZClasses/_pmc.txt 2005-02-07 12:36:01 UTC (rev 29069)
@@ -0,0 +1,169 @@
+Persistent Extension Classes
+============================
+
+The _pmc module provides a meta class that can be used to implement
+persistent extension classes for ZClasses.
+
+Persistent classes have the following properties:
+
+- They cannot be turned into ghosts
+
+- They can only contain picklable subobjects
+
+Let's look at an example:
+
+ >>> def __init__(self, name):
+ ... self.name = name
+
+ >>> def foo(self):
+ ... return self.name, self.kind
+
+ >>> import ZClasses._pmc
+ >>> class C:
+ ... __metaclass__ = ZClasses._pmc.ZClassPersistentMetaClass
+ ... __init__ = __init__
+ ... 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.
+
+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:
+
+ >>> connection = some_database.open()
+ >>> connection.root()['C'] = C
+ >>> import transaction
+ >>> transaction.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:
+
+ >>> transaction.abort()
+
+Then the class will return to it's prior state:
+
+ >>> c.baz()
+ Traceback (most recent call last):
+ ...
+ AttributeError: baz
+
+ >>> c.bar()
+ bar first
+
+We can open another connection and access the class there. Let's do
+that in another thread:
+
+ >>> import threading
+ >>> def run(func):
+ ... thread = threading.Thread(target=func)
+ ... thread.start()
+ ... thread.join()
+
+ >>> def read_class():
+ ... connection = some_database.open()
+ ... C = connection.root()['C']
+ ... c = C('other')
+ ... c.bar()
+ ... connection.close()
+
+ >>> run(read_class)
+ bar other
+
+If we make changes without commiting them:
+
+ >>> C.bar = baz
+ >>> c.bar()
+ baz first
+
+Other connections/threads are unaffected:
+
+ >>> run(read_class)
+ bar other
+
+Until we commit:
+
+ >>> transaction.commit()
+ >>> run(read_class)
+ baz other
+
+Similarly, we don't see changes made in other connextions:
+
+ >>> def write_class():
+ ... connection = some_database.open()
+ ... C = connection.root()['C']
+ ... C.color = 'red'
+ ... transaction.commit()
+ ... connection.close()
+
+ >>> run(write_class)
+
+ >>> c.color
+ Traceback (most recent call last):
+ ...
+ AttributeError: color
+
+until we sync:
+
+ >>> connection.sync()
+ >>> c.color
+ 'red'
+
Property changes on: Zope/branches/jim-fix-zclasses/lib/python/ZClasses/_pmc.txt
___________________________________________________________________
Name: svn:eol-style
+ native
Added: Zope/branches/jim-fix-zclasses/lib/python/ZClasses/tests.py
===================================================================
--- Zope/branches/jim-fix-zclasses/lib/python/ZClasses/tests.py 2005-02-07 12:35:58 UTC (rev 29068)
+++ Zope/branches/jim-fix-zclasses/lib/python/ZClasses/tests.py 2005-02-07 12:36:01 UTC (rev 29069)
@@ -0,0 +1,56 @@
+##############################################################################
+#
+# 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 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((
+
+ # To do:
+ # - test integration: doctest.DocFileSuite("ZClass.txt"),
+ # - Test working with old pickles
+ # - Test export/import
+
+ doctest.DocFileSuite("_pmc.txt", setUp=setUp, tearDown=tearDown),
+ ))
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='test_suite')
+
Property changes on: Zope/branches/jim-fix-zclasses/lib/python/ZClasses/tests.py
___________________________________________________________________
Name: svn:keywords
+ Id
Name: svn:eol-style
+ native
More information about the Zope-Checkins
mailing list