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):
"""
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')
def getUid(self):
"""
......@@ -1662,6 +1685,8 @@ class ERP5Generator(PortalGenerator):
# Return the fully wrapped object.
p = parent.this()._getOb(id)
p._setProperty('version_priority', ('erp5',), 'lines')
erp5_sql_deferred_connection_string = erp5_sql_connection_string
p._setProperty('erp5_catalog_storage',
erp5_catalog_storage, 'string')
......
......@@ -101,9 +101,8 @@ class Component(Base):
fromlist=[namespace_fullname])
reference = self.getReference()
namespace_module._registry_dict[reference] = {
'component': self,
'module_name': "%s.%s" % (namespace_fullname, reference)}
namespace_module._registry_dict.setdefault(
reference, {})[self.getVersion()] = self
security.declareProtected(Permissions.ModifyPortalContent,
'deleteFromRegistry')
......
......@@ -54,6 +54,22 @@ class ComponentTool(BaseTool):
security = ClassSecurityInfo()
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')
def reset(self, force=True):
"""
......@@ -95,16 +111,7 @@ class ComponentTool(BaseTool):
except AttributeError:
pass
else:
for name, klass in module.__dict__.items():
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)
self._resetModule(module)
type_tool.resetDynamicDocumentsOnceAtTransactionBoundary()
......
......@@ -37,6 +37,12 @@ from Products.ERP5.ERP5Site import getSite
from types import ModuleType
from zLOG import LOG, INFO
class ComponentVersionPackage(ModuleType):
"""
Component Version package (erp5.component.XXX.VERSION)
"""
__path__ = []
class ComponentDynamicPackage(ModuleType):
"""
A top-level component is a package as it contains modules, this is required
......@@ -106,9 +112,8 @@ class ComponentDynamicPackage(ModuleType):
# updating the registry
if component.getValidationState() in ('modified', 'validated'):
reference = component.getReference()
self.__registry_dict[reference] = {
'component': component,
'module_name': self._namespace_prefix + reference}
self.__registry_dict.setdefault(
reference, {})[component.getVersion()] = component
return self.__registry_dict
......@@ -119,21 +124,45 @@ class ComponentDynamicPackage(ModuleType):
if path or not fullname.startswith(self._namespace_prefix):
return None
site = getSite()
# __import__ will first try a relative import, for example
# erp5.component.XXX.YYY.ZZZ where erp5.component.XXX.YYY is the current
# Component where an import is done
name = fullname.replace(self._namespace_prefix, '')
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
# wrongly considered as importable and thus the actual filesystem class
# 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 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):
"""
Load a module with given fullname (see PEP 302) if it's not
......@@ -141,17 +170,60 @@ class ComponentDynamicPackage(ModuleType):
properly in find_module().
"""
site = getSite()
component_name = fullname.replace(self._namespace_prefix, '')
component_id = '%s.%s' % (self._namespace, component_name)
try:
component = self._registry_dict[component_name]['component']
except KeyError:
LOG("ERP5Type.dynamic", INFO,
"Could not find %s or it has not been validated or it has not been "
"migrated yet?" % component_id)
if component_name.endswith('_version'):
version = component_name.replace('_version', '')
return (version in site.getVersionPriority() and
self._getVersionPackage(version) or None)
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:
new_module = ModuleType(component_id, component.getDescription())
......@@ -159,19 +231,27 @@ class ComponentDynamicPackage(ModuleType):
# The module *must* be in sys.modules before executing the code in case
# the module code imports (directly or indirectly) itself (see PEP 302)
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)
new_module.__file__ = "<%s>" % component_name
try:
component.load(new_module.__dict__, validated_only=True)
except Exception, e:
except:
del sys.modules[component_id]
if component_id_alias:
del sys.modules[component_id_alias]
raise
new_module.__path__ = []
new_module.__loader__ = self
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
......@@ -42,16 +42,15 @@ from Products.ERP5Type.TransactionalVariable import TransactionalResource
from zLOG import LOG, ERROR, INFO, WARNING, PANIC
def _importClass(classpath, is_zodb_document=False):
def _importClass(classpath):
try:
module_path, class_name = classpath.rsplit('.', 1)
module = __import__(module_path, {}, {}, (module_path,))
klass = getattr(module, class_name)
if not is_zodb_document:
# XXX is this required? (here?)
setDefaultClassProperties(klass)
InitializeClass(klass)
# XXX is this required? (here?)
setDefaultClassProperties(klass)
InitializeClass(klass)
return klass
except StandardError:
......@@ -183,7 +182,6 @@ def generatePortalTypeClass(site, portal_type_name):
raise AttributeError('Document class is not defined on Portal Type %s' \
% portal_type_name)
is_zodb_document = False
klass = None
if '.' in type_class:
type_class_path = type_class
......@@ -195,32 +193,23 @@ def generatePortalTypeClass(site, portal_type_name):
type_class_namespace = document_class_registry.get(type_class, '')
if not (type_class_namespace.startswith('Products.ERP5Type') or
portal_type_name in core_portal_type_class_dict):
import erp5.component.document
module_info_dict = erp5.component.document._registry_dict.get(type_class,
None)
if module_info_dict:
type_class_path = "%s.%s" % (module_info_dict['module_name'], type_class)
is_zodb_document = True
if type_class_path is None:
try:
klass = getattr(__import__('erp5.component.document.%s' % type_class,
fromlist=['erp5.component.document'],
level=0),
type_class)
except (ImportError, AttributeError):
pass
if klass is None:
type_class_path = document_class_registry.get(type_class, None)
if type_class_path is None:
raise AttributeError('Document class %s has not been registered:'
' cannot import it as base of Portal Type %s'
% (type_class, portal_type_name))
try:
klass = _importClass(type_class_path, is_zodb_document)
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
if klass is None:
klass = _importClass(type_class_path)
global property_sheet_generating_portal_type_set
......
......@@ -1209,7 +1209,7 @@ ComponentTool._original_reset = ComponentTool.reset
ComponentTool._reset_performed = False
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):
from Products.ERP5Type.Tool.ComponentTool import ComponentTool
......@@ -1233,7 +1233,7 @@ class _TestZodbComponent(ERP5TypeTestCase):
self._component_tool.reset()
@abc.abstractmethod
def _newComponent(self, reference, text_content):
def _newComponent(self, reference, text_content, version='erp5'):
pass
@abc.abstractmethod
......@@ -1338,12 +1338,114 @@ class _TestZodbComponent(ERP5TypeTestCase):
self.assertEquals(component.getTextContent(validated_only=True), valid_code)
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
class TestZodbExtensionComponent(_TestZodbComponent):
def _newComponent(self, reference, text_content):
def _newComponent(self, reference, text_content, version='erp5'):
return self._component_tool.newContent(
id='%s.%s' % (self._getComponentModuleName(), reference),
id='%s.%s.%s' % (self._getComponentModuleName(),
version + '_version',
reference),
version=version,
reference=reference,
text_content=text_content,
portal_type='Extension Component')
......@@ -1404,10 +1506,12 @@ class TestZodbExtensionComponent(_TestZodbComponent):
from Products.ERP5Type.Core.DocumentComponent import DocumentComponent
class TestZodbDocumentComponent(_TestZodbComponent):
def _newComponent(self, reference, text_content):
def _newComponent(self, reference, text_content, version='erp5'):
return self._component_tool.newContent(
id='%s.%s' % (self._getComponentModuleName(), reference),
id='%s.%s.%s' % (self._getComponentModuleName(),
version + '_version', reference),
reference=reference,
version=version,
text_content=text_content,
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