diff --git a/product/ERP5Type/dynamic/lazy_class.py b/product/ERP5Type/dynamic/lazy_class.py index 61824706a86334803521d6b60029bf332bfd405e..997730cc3d68bc2f4bcfe7efd3715cb3862b6d04 100644 --- a/product/ERP5Type/dynamic/lazy_class.py +++ b/product/ERP5Type/dynamic/lazy_class.py @@ -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,), {}) diff --git a/product/ERP5Type/tests/testDynamicClassGeneration.py b/product/ERP5Type/tests/testDynamicClassGeneration.py index 6e6cb87566eb49ef9aa2ddabd76b11f04b9fbf9a..99a07c5de3afdd1f082786e6b292eb42c079e3f8 100644 --- a/product/ERP5Type/tests/testDynamicClassGeneration.py +++ b/product/ERP5Type/tests/testDynamicClassGeneration.py @@ -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