Commit 5a958dc3 authored by Jim Fulton's avatar Jim Fulton

Added broken-object support.

Broken objects are objects who's class has gone away, typically
because modules or classes have been removed or moved.
parent 9f0bb48f
##############################################################################
#
# 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.
#
##############################################################################
"""Broken object support
$Id: broken.py,v 1.1 2004/02/25 12:31:40 jim Exp $
"""
import sys
import persistent
broken_cache = {}
class Broken(object):
"""Broken object base class
Broken objects are placeholders for objects that can no longer be
created because their class has gone away.
Broken objects don't really do much of anything, except hold their
state. The Broken class is used as a base class for creating
classes in leu of missing classes::
>>> Atall = type('Atall', (Broken, ), {'__module__': 'not.there'})
The only thing the class can be used for is to create new objects::
>>> Atall()
<broken not.there.Atall instance>
>>> Atall().__Broken_newargs__
()
>>> Atall().__Broken_initargs__
()
>>> Atall(1, 2).__Broken_newargs__
(1, 2)
>>> Atall(1, 2).__Broken_initargs__
(1, 2)
>>> a = Atall.__new__(Atall, 1, 2)
>>> a
<broken not.there.Atall instance>
>>> a.__Broken_newargs__
(1, 2)
>>> a.__Broken_initargs__
You can't modify broken objects::
>>> a.x = 1
Traceback (most recent call last):
...
BrokenModified: Can't change broken objects
But you can set their state::
>>> a.__setstate__({'x': 1, })
You can pickle broken objects::
>>> r = a.__reduce__()
>>> len(r)
3
>>> r[0] is rebuild
True
>>> r[1]
('not.there', 'Atall', 1, 2)
>>> r[2]
{'x': 1}
>>> import cPickle
>>> a2 = cPickle.loads(cPickle.dumps(a, 1))
>>> a2
<broken not.there.Atall instance>
>>> a2.__Broken_newargs__
(1, 2)
>>> a2.__Broken_initargs__
>>> a2.__Broken_state__
{'x': 1}
"""
__Broken_state__ = __Broken_initargs__ = None
__name__ = 'bob XXX'
def __new__(class_, *args):
result = object.__new__(class_)
result.__dict__['__Broken_newargs__'] = args
return result
def __init__(self, *args):
self.__dict__['__Broken_initargs__'] = args
def __reduce__(self):
"""We pickle broken objects in hope of being able to fix them later
"""
return (rebuild,
((self.__class__.__module__, self.__class__.__name__)
+ self.__Broken_newargs__),
self.__Broken_state__,
)
def __setstate__(self, state):
self.__dict__['__Broken_state__'] = state
def __repr__(self):
return "<broken %s.%s instance>" % (
self.__class__.__module__, self.__class__.__name__)
def __setattr__(self, name, value):
raise BrokenModified("Can't change broken objects")
def find_global(modulename, globalname, Broken=Broken):
"""Find a global object, returninga broken class if it can't be found
This function looks up global variable in modules::
>>> import sys
>>> find_global('sys', 'path') is sys.path
True
If an object can't be found, a broken class is returned::
>>> broken = find_global('ZODB.not.there', 'atall')
>>> issubclass(broken, Broken)
True
>>> broken.__module__
'ZODB.not.there'
>>> broken.__name__
'atall'
Broken classes are cached::
>>> find_global('ZODB.not.there', 'atall') is broken
True
If we "repair" a missing global::
>>> class ZODBnotthere:
... atall = []
>>> sys.modules['ZODB.not'] = ZODBnotthere
>>> sys.modules['ZODB.not.there'] = ZODBnotthere
we can then get the repaired value::
>>> find_global('ZODB.not.there', 'atall') is ZODBnotthere.atall
True
Of course, if we beak it again::
>>> del sys.modules['ZODB.not']
>>> del sys.modules['ZODB.not.there']
we get the broken value::
>>> find_global('ZODB.not.there', 'atall') is broken
True
"""
try:
__import__(modulename)
except ImportError:
pass
else:
module = sys.modules[modulename]
try:
return getattr(module, globalname)
except AttributeError:
pass
try:
return broken_cache[(modulename, globalname)]
except KeyError:
pass
class_ = type(globalname, (Broken, ), {'__module__': modulename})
broken_cache[(modulename, globalname)] = class_
return class_
def rebuild(modulename, globalname, *args):
"""Recreate a broken object, possibly recreating the missing class
This functions unpickles broken objects::
>>> broken = rebuild('ZODB.notthere', 'atall', 1, 2)
>>> broken
<broken ZODB.notthere.atall instance>
>>> broken.__Broken_newargs__
(1, 2)
If we "repair" the brokenness::
>>> class notthere: # fake notthere module
... class atall(object):
... def __new__(self, *args):
... ob = object.__new__(self)
... ob.args = args
... return ob
... def __repr__(self):
... return 'atall %s %s' % self.args
>>> sys.modules['ZODB.notthere'] = notthere
>>> rebuild('ZODB.notthere', 'atall', 1, 2)
atall 1 2
>>> del sys.modules['ZODB.notthere']
"""
class_ = find_global(modulename, globalname)
return class_.__new__(class_, *args)
class BrokenModified(TypeError):
"""Attempt to modify a broken object
"""
class PersistentBroken(Broken, persistent.Persistent):
r"""Persistent broken objects
Persistent broken objects are used for broken objects that are
also persistent. In addition to having to track the original
object data, they need to handle persistent meta data.
Persistent broken classes are created from existing broken classes
using the persistentBroken, function::
>>> Atall = type('Atall', (Broken, ), {'__module__': 'not.there'})
>>> PAtall = persistentBroken(Atall)
(Note that we always get the *same* persistent broken class
for a given broken class::
>>> persistentBroken(Atall) is PAtall
True
)
Persistent broken classes work a lot like broken classes::
>>> a = PAtall.__new__(PAtall, 1, 2)
>>> a
<persistent broken not.there.Atall instance None>
>>> a.__Broken_newargs__
(1, 2)
>>> a.__Broken_initargs__
>>> a.x = 1
Traceback (most recent call last):
...
BrokenModified: Can't change broken objects
Unlike regular broken objects, persistent broken objects keep
track of persistence meta data:
>>> a._p_oid = '\0\0\0\0****'
>>> a
<persistent broken not.there.Atall instance '\x00\x00\x00\x00****'>
and persistent broken objects aren't directly picklable:
>>> a.__reduce__()
Traceback (most recent call last):
...
BrokenModified: """ \
r"""<persistent broken not.there.Atall instance '\x00\x00\x00\x00****'>
but you can get their state:
>>> a.__setstate__({'y': 2})
>>> a.__getstate__()
{'y': 2}
"""
def __new__(class_, *args):
result = persistent.Persistent.__new__(class_)
result.__dict__['__Broken_newargs__'] = args
return result
def __reduce__(self, *args):
raise BrokenModified(self)
def __getstate__(self):
return self.__Broken_state__
def __setattr__(self, name, value):
if name.startswith('_p_'):
persistent.Persistent.__setattr__(self, name, value)
else:
raise BrokenModified("Can't change broken objects")
def __repr__(self):
return "<persistent broken %s.%s instance %r>" % (
self.__class__.__module__, self.__class__.__name__,
self._p_oid)
def __getnewargs__(self):
return self.__Broken_newargs__
def persistentBroken(class_):
try:
return class_.__dict__['__Broken_Persistent__']
except KeyError:
class_.__Broken_Persistent__ = (
type(class_.__name__,
(PersistentBroken, class_),
{'__module__': class_.__module__},
)
)
return class_.__dict__['__Broken_Persistent__']
...@@ -86,14 +86,16 @@ import cPickle ...@@ -86,14 +86,16 @@ import cPickle
import cStringIO import cStringIO
import logging import logging
from persistent import Persistent from persistent import Persistent
from persistent.wref import WeakRefMarker, WeakRef from persistent.wref import WeakRefMarker, WeakRef
from ZODB import broken
from ZODB.broken import Broken
from ZODB.POSException import InvalidObjectReference from ZODB.POSException import InvalidObjectReference
# Might to update or redo to reflect weakrefs # Might to update or redo coptimizations to reflect weakrefs:
# from ZODB.coptimizations import new_persistent_id # from ZODB.coptimizations import new_persistent_id
def myhasattr(obj, name, _marker=object()): def myhasattr(obj, name, _marker=object()):
"""Make sure we don't mask exceptions like hasattr(). """Make sure we don't mask exceptions like hasattr().
...@@ -369,13 +371,20 @@ class BaseObjectReader: ...@@ -369,13 +371,20 @@ class BaseObjectReader:
if isinstance(klass, tuple): if isinstance(klass, tuple):
# Old module_name, class_name tuple # Old module_name, class_name tuple
klass = self._get_class(*klass) klass = self._get_class(*klass)
if args is None: if args is None:
return klass.__new__(klass) args = ()
else:
return klass.__new__(klass, *args)
else: else:
# Definately new style direct class reference # Definately new style direct class reference
return klass.__new__(klass) args = ()
if issubclass(klass, Broken):
# We got a broken class. We might need to make it
# PersistentBroken
if not issubclass(klass, broken.PersistentBroken):
klass = broken.persistentBroken(klass)
return klass.__new__(klass, *args)
def getState(self, pickle): def getState(self, pickle):
unpickler = self._get_unpickler(pickle) unpickler = self._get_unpickler(pickle)
...@@ -420,16 +429,37 @@ class ConnectionObjectReader(BaseObjectReader): ...@@ -420,16 +429,37 @@ class ConnectionObjectReader(BaseObjectReader):
def _get_class(self, module, name): def _get_class(self, module, name):
return self._factory(self._conn, module, name) return self._factory(self._conn, module, name)
def _get_unpickler(self, pickle):
unpickler = BaseObjectReader._get_unpickler(self, pickle)
factory = self._factory
conn = self._conn
def find_global(modulename, name):
return factory(conn, modulename, name)
unpickler.find_global = find_global
return unpickler
def _persistent_load(self, oid): def _persistent_load(self, oid):
if isinstance(oid, tuple): if isinstance(oid, tuple):
# Quick instance reference. We know all we need to know # Quick instance reference. We know all we need to know
# to create the instance w/o hitting the db, so go for it! # to create the instance w/o hitting the db, so go for it!
oid, klass = oid oid, klass = oid
obj = self._cache.get(oid, None) # XXX it's not a dict obj = self._cache.get(oid, None) # XXX it's not a dict
if obj is not None: if obj is not None:
return obj return obj
if isinstance(klass, tuple): if isinstance(klass, tuple):
klass = self._get_class(*klass) klass = self._get_class(*klass)
if issubclass(klass, Broken):
# We got a broken class. We might need to make it
# PersistentBroken
if not issubclass(klass, broken.PersistentBroken):
klass = broken.persistentBroken(klass)
try: try:
obj = klass.__new__(klass) obj = klass.__new__(klass)
except TypeError: except TypeError:
......
##############################################################################
#
# 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.
#
##############################################################################
"""Test broken-object suppport
$Id: testBroken.py,v 1.1 2004/02/25 12:31:41 jim Exp $
"""
import sys
import unittest
import persistent
from transaction import get_transaction
from doctest import DocTestSuite
from ZODB.tests.util import DB
def test_integration():
"""Test the integration of broken object support with the databse:
>>> db = DB()
We'll create a fake module with a class:
>>> class NotThere:
... Atall = type('Atall', (persistent.Persistent, ),
... {'__module__': 'ZODB.not.there'})
And stuff this into sys.modules to simulate a regular module:
>>> sys.modules['ZODB.not.there'] = NotThere
>>> sys.modules['ZODB.not'] = NotThere
Now, we'll create and save an instance, and make sure we can
load it in another connection:
>>> a = NotThere.Atall()
>>> a.x = 1
>>> conn1 = db.open()
>>> conn1.root()['a'] = a
>>> get_transaction().commit()
>>> conn2 = db.open()
>>> a2 = conn2.root()['a']
>>> a2.__class__ is a.__class__
True
>>> a2.x
1
Now, we'll uninstall the module, simulating having the module
go away:
>>> del sys.modules['ZODB.not.there']
and we'll try to load the object in another connection:
>>> conn3 = db.open()
>>> a3 = conn3.root()['a']
>>> a3
<persistent broken ZODB.not.there.Atall instance """ \
r"""'\x00\x00\x00\x00\x00\x00\x00\x01'>
>>> a3.__Broken_state__
{'x': 1}
>>> db.close()
"""
def test_suite():
return unittest.TestSuite((
DocTestSuite('ZODB.broken'),
DocTestSuite(),
))
if __name__ == '__main__': unittest.main()
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