Commit d1b12de0 authored by Jim Fulton's avatar Jim Fulton

Created a meta class for persistent classes. This is based very

loosly on the experimental persistent class code from Zope 3.
parent f197316c
##############################################################################
#
# 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)
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'
##############################################################################
#
# 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')
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment