Commit b53f29a3 authored by Arnaud Fontaine's avatar Arnaud Fontaine

Implement Component versioning.

An appropriate Python package is created in erp5.component.document.
erp5.component.document.COMPONENT_REFERENCE is just an alias for
erp5.component.document.VERSION.COMPONENT_REFERENCE where VERSION is the
version with the highest priority (as defined by priority property on
ComponentTool) where the component is available.
parent d50901ba
...@@ -434,6 +434,29 @@ class ERP5Site(FolderMixIn, CMFSite, CacheCookieMixin): ...@@ -434,6 +434,29 @@ class ERP5Site(FolderMixIn, CMFSite, CacheCookieMixin):
""" """
return self.title return self.title
security.declareProtected(Permissions.AccessContentsInformation,
'getVersionPriority')
def getVersionPriority(self):
# Whatever happens, a version must always be returned otherwise it may
# render the site unusable when all Products will have been migrated
if not self._version_priority:
return ('erp5',)
return self._version_priority
security.declareProtected(Permissions.ModifyPortalContent,
'setVersionPriority' )
def setVersionPriority(self, value):
"""
XXX-arnau: really hackish...
"""
self._version_priority = value
if not getattr(self, '_v_bootstrapping', False):
self.portal_components.resetOnceAtTransactionBoundary()
version_priority = property(getVersionPriority, setVersionPriority)
security.declareProtected(Permissions.AccessContentsInformation, 'getUid') security.declareProtected(Permissions.AccessContentsInformation, 'getUid')
def getUid(self): def getUid(self):
""" """
...@@ -1662,6 +1685,8 @@ class ERP5Generator(PortalGenerator): ...@@ -1662,6 +1685,8 @@ class ERP5Generator(PortalGenerator):
# Return the fully wrapped object. # Return the fully wrapped object.
p = parent.this()._getOb(id) p = parent.this()._getOb(id)
p._setProperty('version_priority', ('erp5',), 'lines')
erp5_sql_deferred_connection_string = erp5_sql_connection_string erp5_sql_deferred_connection_string = erp5_sql_connection_string
p._setProperty('erp5_catalog_storage', p._setProperty('erp5_catalog_storage',
erp5_catalog_storage, 'string') erp5_catalog_storage, 'string')
......
...@@ -101,9 +101,8 @@ class Component(Base): ...@@ -101,9 +101,8 @@ class Component(Base):
fromlist=[namespace_fullname]) fromlist=[namespace_fullname])
reference = self.getReference() reference = self.getReference()
namespace_module._registry_dict[reference] = { namespace_module._registry_dict.setdefault(
'component': self, reference, {})[self.getVersion()] = self
'module_name': "%s.%s" % (namespace_fullname, reference)}
security.declareProtected(Permissions.ModifyPortalContent, security.declareProtected(Permissions.ModifyPortalContent,
'deleteFromRegistry') 'deleteFromRegistry')
......
...@@ -54,6 +54,22 @@ class ComponentTool(BaseTool): ...@@ -54,6 +54,22 @@ class ComponentTool(BaseTool):
security = ClassSecurityInfo() security = ClassSecurityInfo()
security.declareObjectProtected(Permissions.AccessContentsInformation) security.declareObjectProtected(Permissions.AccessContentsInformation)
def _resetModule(self, module):
for name, klass in module.__dict__.items():
if not (name[0] != '_' and isinstance(klass, ModuleType)):
continue
full_module_name = "%s.%s" % (module.__name__, name)
LOG("ERP5Type.Tool.ComponentTool", INFO, "Resetting " + full_module_name)
if name.endswith('_version'):
self._resetModule(getattr(module, name))
# The module must be deleted first
del sys.modules[full_module_name]
delattr(module, name)
security.declareProtected(Permissions.ModifyPortalContent, 'reset') security.declareProtected(Permissions.ModifyPortalContent, 'reset')
def reset(self, force=True): def reset(self, force=True):
""" """
...@@ -95,16 +111,7 @@ class ComponentTool(BaseTool): ...@@ -95,16 +111,7 @@ class ComponentTool(BaseTool):
except AttributeError: except AttributeError:
pass pass
else: else:
for name, klass in module.__dict__.items(): self._resetModule(module)
if name[0] != '_' and isinstance(klass, ModuleType):
full_module_name = "erp5.component.%s.%s" % (module_name, name)
LOG("ERP5Type.Tool.ComponentTool", INFO,
"Resetting " + full_module_name)
# The module must be deleted first
del sys.modules[full_module_name]
delattr(module, name)
type_tool.resetDynamicDocumentsOnceAtTransactionBoundary() type_tool.resetDynamicDocumentsOnceAtTransactionBoundary()
......
...@@ -37,6 +37,12 @@ from Products.ERP5.ERP5Site import getSite ...@@ -37,6 +37,12 @@ from Products.ERP5.ERP5Site import getSite
from types import ModuleType from types import ModuleType
from zLOG import LOG, INFO from zLOG import LOG, INFO
class ComponentVersionPackage(ModuleType):
"""
Component Version package (erp5.component.XXX.VERSION)
"""
__path__ = []
class ComponentDynamicPackage(ModuleType): class ComponentDynamicPackage(ModuleType):
""" """
A top-level component is a package as it contains modules, this is required A top-level component is a package as it contains modules, this is required
...@@ -106,9 +112,8 @@ class ComponentDynamicPackage(ModuleType): ...@@ -106,9 +112,8 @@ class ComponentDynamicPackage(ModuleType):
# updating the registry # updating the registry
if component.getValidationState() in ('modified', 'validated'): if component.getValidationState() in ('modified', 'validated'):
reference = component.getReference() reference = component.getReference()
self.__registry_dict[reference] = { self.__registry_dict.setdefault(
'component': component, reference, {})[component.getVersion()] = component
'module_name': self._namespace_prefix + reference}
return self.__registry_dict return self.__registry_dict
...@@ -119,21 +124,45 @@ class ComponentDynamicPackage(ModuleType): ...@@ -119,21 +124,45 @@ class ComponentDynamicPackage(ModuleType):
if path or not fullname.startswith(self._namespace_prefix): if path or not fullname.startswith(self._namespace_prefix):
return None return None
site = getSite()
# __import__ will first try a relative import, for example # __import__ will first try a relative import, for example
# erp5.component.XXX.YYY.ZZZ where erp5.component.XXX.YYY is the current # erp5.component.XXX.YYY.ZZZ where erp5.component.XXX.YYY is the current
# Component where an import is done # Component where an import is done
name = fullname.replace(self._namespace_prefix, '') name = fullname.replace(self._namespace_prefix, '')
if '.' in name: if '.' in name:
return None try:
version, name = name.split('.')
version = version.replace('_version', '')
except ValueError:
return None
try:
self._registry_dict[name][version]
except KeyError:
return None
# Skip components not available, otherwise Products for example could be # Skip components not available, otherwise Products for example could be
# wrongly considered as importable and thus the actual filesystem class # wrongly considered as importable and thus the actual filesystem class
# ignored # ignored
if name not in self._registry_dict: elif (name not in self._registry_dict and
name.replace('_version', '') not in site.getVersionPriority()):
return None return None
return self return self
def _getVersionPackage(self, version):
version += '_version'
version_package = getattr(self, version, None)
if version_package is None:
version_package_name = '%s.%s' % (self._namespace, version)
version_package = ComponentVersionPackage(version_package_name)
sys.modules[version_package_name] = version_package
setattr(self, version, version_package)
return version_package
def load_module(self, fullname): def load_module(self, fullname):
""" """
Load a module with given fullname (see PEP 302) if it's not Load a module with given fullname (see PEP 302) if it's not
...@@ -141,17 +170,60 @@ class ComponentDynamicPackage(ModuleType): ...@@ -141,17 +170,60 @@ class ComponentDynamicPackage(ModuleType):
properly in find_module(). properly in find_module().
""" """
site = getSite() site = getSite()
component_name = fullname.replace(self._namespace_prefix, '') component_name = fullname.replace(self._namespace_prefix, '')
component_id = '%s.%s' % (self._namespace, component_name) if component_name.endswith('_version'):
try: version = component_name.replace('_version', '')
component = self._registry_dict[component_name]['component'] return (version in site.getVersionPriority() and
except KeyError: self._getVersionPackage(version) or None)
LOG("ERP5Type.dynamic", INFO,
"Could not find %s or it has not been validated or it has not been "
"migrated yet?" % component_id)
return None component_id_alias = None
version_package_name = component_name.replace('_version', '')
if '.' in component_name:
try:
version, component_name = component_name.split('.')
version = version.replace('_version', '')
except ValueError:
return None
try:
component = self._registry_dict[component_name][version]
except KeyError:
LOG("ERP5Type.dynamic", INFO,
"Could not find version %s of Component %s" % (version,
component_name))
return None
else:
try:
component_version_dict = self._registry_dict[component_name]
except KeyError:
LOG("ERP5Type.dynamic", INFO,
"Could not find Component " + component_name)
return None
for version in site.getVersionPriority():
component = component_version_dict.get(version, None)
if component is not None:
break
if component is None:
return None
try:
module = getattr(getattr(self, version + '_version'), component_name)
except AttributeError:
pass
else:
with self._lock:
setattr(self._getVersionPackage(version), component_name, module)
return module
component_id_alias = '%s.%s' % (self._namespace, component_name)
component_id = '%s.%s_version.%s' % (self._namespace, version,
component_name)
with self._lock: with self._lock:
new_module = ModuleType(component_id, component.getDescription()) new_module = ModuleType(component_id, component.getDescription())
...@@ -159,19 +231,27 @@ class ComponentDynamicPackage(ModuleType): ...@@ -159,19 +231,27 @@ class ComponentDynamicPackage(ModuleType):
# The module *must* be in sys.modules before executing the code in case # The module *must* be in sys.modules before executing the code in case
# the module code imports (directly or indirectly) itself (see PEP 302) # the module code imports (directly or indirectly) itself (see PEP 302)
sys.modules[component_id] = new_module sys.modules[component_id] = new_module
if component_id_alias:
sys.modules[component_id_alias] = new_module
# This must be set for imports at least (see PEP 302) # This must be set for imports at least (see PEP 302)
new_module.__file__ = "<%s>" % component_name new_module.__file__ = "<%s>" % component_name
try: try:
component.load(new_module.__dict__, validated_only=True) component.load(new_module.__dict__, validated_only=True)
except Exception, e: except:
del sys.modules[component_id] del sys.modules[component_id]
if component_id_alias:
del sys.modules[component_id_alias]
raise raise
new_module.__path__ = [] new_module.__path__ = []
new_module.__loader__ = self new_module.__loader__ = self
new_module.__name__ = component_id new_module.__name__ = component_id
setattr(self, component_name, new_module) setattr(self._getVersionPackage(version), component_name, new_module)
if component_id_alias:
setattr(self, component_name, new_module)
return new_module return new_module
...@@ -42,16 +42,15 @@ from Products.ERP5Type.TransactionalVariable import TransactionalResource ...@@ -42,16 +42,15 @@ from Products.ERP5Type.TransactionalVariable import TransactionalResource
from zLOG import LOG, ERROR, INFO, WARNING, PANIC from zLOG import LOG, ERROR, INFO, WARNING, PANIC
def _importClass(classpath, is_zodb_document=False): def _importClass(classpath):
try: try:
module_path, class_name = classpath.rsplit('.', 1) module_path, class_name = classpath.rsplit('.', 1)
module = __import__(module_path, {}, {}, (module_path,)) module = __import__(module_path, {}, {}, (module_path,))
klass = getattr(module, class_name) klass = getattr(module, class_name)
if not is_zodb_document: # XXX is this required? (here?)
# XXX is this required? (here?) setDefaultClassProperties(klass)
setDefaultClassProperties(klass) InitializeClass(klass)
InitializeClass(klass)
return klass return klass
except StandardError: except StandardError:
...@@ -183,7 +182,6 @@ def generatePortalTypeClass(site, portal_type_name): ...@@ -183,7 +182,6 @@ def generatePortalTypeClass(site, portal_type_name):
raise AttributeError('Document class is not defined on Portal Type %s' \ raise AttributeError('Document class is not defined on Portal Type %s' \
% portal_type_name) % portal_type_name)
is_zodb_document = False
klass = None klass = None
if '.' in type_class: if '.' in type_class:
type_class_path = type_class type_class_path = type_class
...@@ -195,32 +193,23 @@ def generatePortalTypeClass(site, portal_type_name): ...@@ -195,32 +193,23 @@ def generatePortalTypeClass(site, portal_type_name):
type_class_namespace = document_class_registry.get(type_class, '') type_class_namespace = document_class_registry.get(type_class, '')
if not (type_class_namespace.startswith('Products.ERP5Type') or if not (type_class_namespace.startswith('Products.ERP5Type') or
portal_type_name in core_portal_type_class_dict): portal_type_name in core_portal_type_class_dict):
import erp5.component.document try:
module_info_dict = erp5.component.document._registry_dict.get(type_class, klass = getattr(__import__('erp5.component.document.%s' % type_class,
None) fromlist=['erp5.component.document'],
if module_info_dict: level=0),
type_class_path = "%s.%s" % (module_info_dict['module_name'], type_class) type_class)
is_zodb_document = True except (ImportError, AttributeError):
pass
if type_class_path is None:
if klass is None:
type_class_path = document_class_registry.get(type_class, None) type_class_path = document_class_registry.get(type_class, None)
if type_class_path is None: if type_class_path is None:
raise AttributeError('Document class %s has not been registered:' raise AttributeError('Document class %s has not been registered:'
' cannot import it as base of Portal Type %s' ' cannot import it as base of Portal Type %s'
% (type_class, portal_type_name)) % (type_class, portal_type_name))
try: if klass is None:
klass = _importClass(type_class_path, is_zodb_document) klass = _importClass(type_class_path)
except ImportError:
# A Document Component should always have a class matching its reference,
# so this should never happen...
if is_zodb_document:
type_class_path = document_class_registry.get(type_class, None)
if type_class_path is not None:
klass = _importClass(type_class_path)
if klass is None:
raise
global property_sheet_generating_portal_type_set global property_sheet_generating_portal_type_set
......
...@@ -1209,7 +1209,7 @@ ComponentTool._original_reset = ComponentTool.reset ...@@ -1209,7 +1209,7 @@ ComponentTool._original_reset = ComponentTool.reset
ComponentTool._reset_performed = False ComponentTool._reset_performed = False
def assertResetNotCalled(*args, **kwargs): def assertResetNotCalled(*args, **kwargs):
raise AssertionError("reset should only be called once revalidating") raise AssertionError("reset should not have been performed")
def assertResetCalled(self, *args, **kwargs): def assertResetCalled(self, *args, **kwargs):
from Products.ERP5Type.Tool.ComponentTool import ComponentTool from Products.ERP5Type.Tool.ComponentTool import ComponentTool
...@@ -1233,7 +1233,7 @@ class _TestZodbComponent(ERP5TypeTestCase): ...@@ -1233,7 +1233,7 @@ class _TestZodbComponent(ERP5TypeTestCase):
self._component_tool.reset() self._component_tool.reset()
@abc.abstractmethod @abc.abstractmethod
def _newComponent(self, reference, text_content): def _newComponent(self, reference, text_content, version='erp5'):
pass pass
@abc.abstractmethod @abc.abstractmethod
...@@ -1338,12 +1338,114 @@ class _TestZodbComponent(ERP5TypeTestCase): ...@@ -1338,12 +1338,114 @@ class _TestZodbComponent(ERP5TypeTestCase):
self.assertEquals(component.getTextContent(validated_only=True), valid_code) self.assertEquals(component.getTextContent(validated_only=True), valid_code)
self.assertModuleImportable('TestComponentWithSyntaxError') self.assertModuleImportable('TestComponentWithSyntaxError')
def testImportVersionedComponentOnly(self):
component = self._newComponent(
'TestImportedVersionedComponentOnly',
"""def foo(*args, **kwargs):
return "TestImportedVersionedComponentOnly"
""")
component.validate()
transaction.commit()
self.tic()
top_module_name = self._getComponentModuleName()
component_import = self._newComponent(
'TestImportVersionedComponentOnly',
"""from %s.erp5_version.TestImportedVersionedComponentOnly import foo
def bar(*args, **kwargs):
return foo(*args, **kwargs)
""" % top_module_name)
component_import.validate()
transaction.commit()
self.tic()
self.assertModuleImportable('TestImportVersionedComponentOnly')
self.assertModuleImportable('erp5_version.TestImportedVersionedComponentOnly')
top_module = __import__(top_module_name, level=0,
fromlist=[top_module_name])
self.assertHasAttribute(
top_module.erp5_version.TestImportedVersionedComponentOnly, 'foo')
self.assertEquals(
top_module.erp5_version.TestImportedVersionedComponentOnly.foo(),
'TestImportedVersionedComponentOnly')
self.failIfHasAttribute(top_module, 'TestImportedVersionedComponentOnly')
def testVersionPriority(self):
component_erp5_version = self._newComponent(
'TestVersionPriority',
"""def function_foo(*args, **kwargs):
return "TestERP5VersionPriority"
""")
component_erp5_version.validate()
transaction.commit()
self.tic()
component_foo_version = self._newComponent(
'TestVersionPriority',
"""def function_foo(*args, **kwargs):
return "TestFooVersionPriority"
""",
'foo')
component_foo_version.validate()
transaction.commit()
self.tic()
self.assertModuleImportable('TestVersionPriority')
self.assertModuleImportable('erp5_version.TestVersionPriority')
self.failIfModuleImportable('foo_version.TestVersionPriority')
top_module_name = self._getComponentModuleName()
top_module = __import__(top_module_name, level=0,
fromlist=[top_module_name])
self.assertHasAttribute(top_module.TestVersionPriority, 'function_foo')
self.assertEquals(top_module.TestVersionPriority.function_foo(),
"TestERP5VersionPriority")
from Products.ERP5.ERP5Site import getSite
site = getSite()
ComponentTool.reset = assertResetCalled
priority_tuple = site.getVersionPriority()
try:
site.setVersionPriority(('foo',) + priority_tuple)
transaction.commit()
self.tic()
self.assertEquals(ComponentTool._reset_performed, True)
self.assertModuleImportable('TestVersionPriority')
self.assertModuleImportable('erp5_version.TestVersionPriority')
self.assertModuleImportable('foo_version.TestVersionPriority')
self.assertHasAttribute(top_module.TestVersionPriority, 'function_foo')
self.assertEquals(top_module.TestVersionPriority.function_foo(),
"TestFooVersionPriority")
finally:
ComponentTool.reset = ComponentTool._original_reset
site.setVersionPriority(priority_tuple)
transaction.commit()
self.tic()
from Products.ERP5Type.Core.ExtensionComponent import ExtensionComponent from Products.ERP5Type.Core.ExtensionComponent import ExtensionComponent
class TestZodbExtensionComponent(_TestZodbComponent): class TestZodbExtensionComponent(_TestZodbComponent):
def _newComponent(self, reference, text_content): def _newComponent(self, reference, text_content, version='erp5'):
return self._component_tool.newContent( return self._component_tool.newContent(
id='%s.%s' % (self._getComponentModuleName(), reference), id='%s.%s.%s' % (self._getComponentModuleName(),
version + '_version',
reference),
version=version,
reference=reference, reference=reference,
text_content=text_content, text_content=text_content,
portal_type='Extension Component') portal_type='Extension Component')
...@@ -1404,10 +1506,12 @@ class TestZodbExtensionComponent(_TestZodbComponent): ...@@ -1404,10 +1506,12 @@ class TestZodbExtensionComponent(_TestZodbComponent):
from Products.ERP5Type.Core.DocumentComponent import DocumentComponent from Products.ERP5Type.Core.DocumentComponent import DocumentComponent
class TestZodbDocumentComponent(_TestZodbComponent): class TestZodbDocumentComponent(_TestZodbComponent):
def _newComponent(self, reference, text_content): def _newComponent(self, reference, text_content, version='erp5'):
return self._component_tool.newContent( return self._component_tool.newContent(
id='%s.%s' % (self._getComponentModuleName(), reference), id='%s.%s.%s' % (self._getComponentModuleName(),
version + '_version', reference),
reference=reference, reference=reference,
version=version,
text_content=text_content, text_content=text_content,
portal_type='Document Component') portal_type='Document Component')
......
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