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 ...@@ -18,8 +18,14 @@ ERP5BaseBroken = type('ERP5BaseBroken', (Broken, ERP5Base), dict(x
for x in PersistentBroken.__dict__.iteritems() for x in PersistentBroken.__dict__.iteritems()
if x[0] not in ('__dict__', '__module__', '__weakref__'))) 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. 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 When an instance of this portal type class is loaded (a new object is
...@@ -31,33 +37,42 @@ class GhostPortalType(ERP5Base): #SimpleItem ...@@ -31,33 +37,42 @@ class GhostPortalType(ERP5Base): #SimpleItem
load, a portal type class does not use GhostPortalType in its __bases__ load, a portal type class does not use GhostPortalType in its __bases__
anymore. anymore.
""" """
def __init__(self, *args, **kw): def __init__(cls, name, bases, dictionary):
self.__class__.loadClass() super(GhostBaseMetaClass, cls).__init__(name, bases, dictionary)
getattr(self, '__init__')(*args, **kw)
def __init__(self, *args, **kw):
def __getattribute__(self, attr): self.__class__.loadClass()
""" getattr(self, '__init__')(*args, **kw)
This is only called once to load the class.
Because __bases__ is changed, the behavior of this object def __getattribute__(self, attr):
will change after the first call. """
""" This is only called once to load the class.
# Class must be loaded if '__of__' is requested because otherwise, Because __bases__ is changed, the behavior of this object
# next call to __getattribute__ would lose any acquisition wrapper. will change after the first call.
if attr in ('__class__', """
'__getnewargs__', # Class must be loaded if '__of__' is requested because otherwise,
'__getstate__', # next call to __getattribute__ would lose any acquisition wrapper.
'__dict__', if attr in ('__class__',
'__module__', '__getnewargs__',
'__name__', '__getstate__',
'__repr__', '__dict__',
'__str__') or attr[:3] in ('_p_', '_v_'): '__module__',
return super(GhostPortalType, self).__getattribute__(attr) '__name__',
#LOG("ERP5Type.Dynamic", BLATHER, '__repr__',
# "loading attribute %s.%s..." % (name, attr)) '__str__') or attr[:3] in ('_p_', '_v_'):
self.__class__.loadClass() return super(cls, self).__getattribute__(attr)
return getattr(self, attr) #LOG("ERP5Type.Dynamic", BLATHER,
# "loading attribute %s.%s..." % (name, attr))
class PortalTypeMetaClass(ExtensionClass): 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 Meta class that is used by portal type classes
...@@ -79,7 +94,7 @@ class PortalTypeMetaClass(ExtensionClass): ...@@ -79,7 +94,7 @@ class PortalTypeMetaClass(ExtensionClass):
PortalTypeMetaClass.subclass_register.setdefault(parent, []).append(cls) PortalTypeMetaClass.subclass_register.setdefault(parent, []).append(cls)
cls.__ghostbase__ = None cls.__ghostbase__ = None
super(PortalTypeMetaClass, cls).__init__(name, bases, dictionary) super(GhostBaseMetaClass, cls).__init__(name, bases, dictionary)
@classmethod @classmethod
def getSubclassList(metacls, cls): def getSubclassList(metacls, cls):
...@@ -124,7 +139,9 @@ class PortalTypeMetaClass(ExtensionClass): ...@@ -124,7 +139,9 @@ class PortalTypeMetaClass(ExtensionClass):
'__ghostbase__', '__ghostbase__',
'portal_type'): 'portal_type'):
delattr(cls, attr) 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.__ghostbase__ = None
cls.resetAcquisitionAndSecurity() cls.resetAcquisitionAndSecurity()
...@@ -193,4 +210,4 @@ class PortalTypeMetaClass(ExtensionClass): ...@@ -193,4 +210,4 @@ class PortalTypeMetaClass(ExtensionClass):
ERP5Base.aq_method_lock.release() ERP5Base.aq_method_lock.release()
def generateLazyPortalTypeClass(portal_type_name): def generateLazyPortalTypeClass(portal_type_name):
return PortalTypeMetaClass(portal_type_name, (GhostPortalType,), {}) return PortalTypeMetaClass(portal_type_name, (InitGhostBase,), {})
...@@ -240,6 +240,51 @@ class TestPortalTypeClass(ERP5TypeTestCase): ...@@ -240,6 +240,51 @@ class TestPortalTypeClass(ERP5TypeTestCase):
implemented_by = list(implementedBy(InterfaceTestType)) implemented_by = list(implementedBy(InterfaceTestType))
self.failIf(IForTest in implemented_by) 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): class TestZodbPropertySheet(ERP5TypeTestCase):
""" """
XXX: WORK IN PROGRESS 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