Commit 568ad1bf authored by Nicolas Dumazet's avatar Nicolas Dumazet

Change ghost behavior so that a ghost always derives from the old bases.

When a portal type class is reset, the ghost that will be used should not
be a "basic" ghost (InitGhostBase) only deriving from Base, or we will have
a lot of code breaking. This ghost should of course have a __getattribute__
method making sure that we get out of ghost state as soon as possible, but most
of all, the __bases__ of this ghost should be the __bases__ of the old portal
type.

In this way, the class behavior before and after a restoreGhostState should not
change, pending un-ghostification. In particular, class attribute lookups will
still return the correct values. (only instance attribute lookups can trigger
a loadClass call)


git-svn-id: https://svn.erp5.org/repos/public/erp5/trunk@40827 20353a03-c40f-0410-a6d1-a30d3c3de9de
parent 34c5244d
......@@ -18,8 +18,14 @@ ERP5BaseBroken = type('ERP5BaseBroken', (Broken, ERP5Base), dict(x
for x in PersistentBroken.__dict__.iteritems()
if x[0] not in ('__dict__', '__module__', '__weakref__')))
class GhostPortalType(ERP5Base): #SimpleItem
class GhostBaseMetaClass(ExtensionClass):
"""
Generate classes that will be used as bases of portal types to
mark portal types as non-loaded and to force loading it.
"""
ghost_doc = """\
Ghost state for a portal type class that is not loaded.
When an instance of this portal type class is loaded (a new object is
......@@ -31,33 +37,42 @@ class GhostPortalType(ERP5Base): #SimpleItem
load, a portal type class does not use GhostPortalType in its __bases__
anymore.
"""
def __init__(self, *args, **kw):
self.__class__.loadClass()
getattr(self, '__init__')(*args, **kw)
def __getattribute__(self, attr):
"""
This is only called once to load the class.
Because __bases__ is changed, the behavior of this object
will change after the first call.
"""
# Class must be loaded if '__of__' is requested because otherwise,
# next call to __getattribute__ would lose any acquisition wrapper.
if attr in ('__class__',
'__getnewargs__',
'__getstate__',
'__dict__',
'__module__',
'__name__',
'__repr__',
'__str__') or attr[:3] in ('_p_', '_v_'):
return super(GhostPortalType, self).__getattribute__(attr)
#LOG("ERP5Type.Dynamic", BLATHER,
# "loading attribute %s.%s..." % (name, attr))
self.__class__.loadClass()
return getattr(self, attr)
class PortalTypeMetaClass(ExtensionClass):
def __init__(cls, name, bases, dictionary):
super(GhostBaseMetaClass, cls).__init__(name, bases, dictionary)
def __init__(self, *args, **kw):
self.__class__.loadClass()
getattr(self, '__init__')(*args, **kw)
def __getattribute__(self, attr):
"""
This is only called once to load the class.
Because __bases__ is changed, the behavior of this object
will change after the first call.
"""
# Class must be loaded if '__of__' is requested because otherwise,
# next call to __getattribute__ would lose any acquisition wrapper.
if attr in ('__class__',
'__getnewargs__',
'__getstate__',
'__dict__',
'__module__',
'__name__',
'__repr__',
'__str__') or attr[:3] in ('_p_', '_v_'):
return super(cls, self).__getattribute__(attr)
#LOG("ERP5Type.Dynamic", BLATHER,
# "loading attribute %s.%s..." % (name, attr))
self.__class__.loadClass()
return getattr(self, attr)
cls.__getattribute__ = __getattribute__
cls.__init__ = __init__
cls.__doc__ = GhostBaseMetaClass.ghost_doc
InitGhostBase = GhostBaseMetaClass('InitGhostBase', (ERP5Base,), {})
class PortalTypeMetaClass(GhostBaseMetaClass):
"""
Meta class that is used by portal type classes
......@@ -79,7 +94,7 @@ class PortalTypeMetaClass(ExtensionClass):
PortalTypeMetaClass.subclass_register.setdefault(parent, []).append(cls)
cls.__ghostbase__ = None
super(PortalTypeMetaClass, cls).__init__(name, bases, dictionary)
super(GhostBaseMetaClass, cls).__init__(name, bases, dictionary)
@classmethod
def getSubclassList(metacls, cls):
......@@ -124,7 +139,9 @@ class PortalTypeMetaClass(ExtensionClass):
'__ghostbase__',
'portal_type'):
delattr(cls, attr)
cls.__bases__ = cls.__ghostbase__
# generate a ghostbase that derives from all previous bases
ghostbase = GhostBaseMetaClass('GhostBase', cls.__bases__, {})
cls.__bases__ = (ghostbase,)
cls.__ghostbase__ = None
cls.resetAcquisitionAndSecurity()
......@@ -193,4 +210,4 @@ class PortalTypeMetaClass(ExtensionClass):
ERP5Base.aq_method_lock.release()
def generateLazyPortalTypeClass(portal_type_name):
return PortalTypeMetaClass(portal_type_name, (GhostPortalType,), {})
return PortalTypeMetaClass(portal_type_name, (InitGhostBase,), {})
......@@ -240,6 +240,51 @@ class TestPortalTypeClass(ERP5TypeTestCase):
implemented_by = list(implementedBy(InterfaceTestType))
self.failIf(IForTest in implemented_by)
def testClassHierarchyAfterReset(self):
"""
Check that after a class reset, the class hierarchy is unchanged until
un-ghostification happens. This is very important for multithreaded
environments:
Thread A. reset dynamic classes
Thread B. in Folder code for instance: CMFBTreeFolder.method(self)
If a reset happens before the B) method call, and does not keep the
correct hierarchy (for instance Folder superclass is removed from
the mro()), a TypeError might be raised:
"method expected CMFBTreeFolder instance, got erp5.portal_type.xxx
instead"
This used to be broken because the ghost state was only what is called
lazy_class.InitGhostBase: a "simple" subclass of ERP5Type.Base
"""
name = "testClassHierarchyAfterReset Module"
types_tool = self.portal.portal_types
ptype = types_tool.newContent(id=name, type_class="Folder")
transaction.commit()
module_class = types_tool.getPortalTypeClass(name)
module_class.loadClass()
# first manually reset and check that everything works
from Products.ERP5Type.Core.Folder import Folder
self.assertTrue(issubclass(module_class, Folder))
synchronizeDynamicModules(self.portal, force=True)
self.assertTrue(issubclass(module_class, Folder))
# then change the type value to something not descending from Folder
# and check behavior
ptype.setTypeClass('Address')
# while the class has not been reset is should still descend from Folder
self.assertTrue(issubclass(module_class, Folder))
# finish transaction and trigger workflow/DynamicModule reset
transaction.commit()
# while the class has not been unghosted it's still a Folder
self.assertTrue(issubclass(module_class, Folder))
# but it changes as soon as the class is loaded
module_class.loadClass()
self.assertFalse(issubclass(module_class, Folder))
class TestZodbPropertySheet(ERP5TypeTestCase):
"""
XXX: WORK IN PROGRESS
......
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