Commit d25f925b authored by Jérome Perrin's avatar Jérome Perrin Committed by Arnaud Fontaine

feat/bt-py3 branch (MR 1979): BusinessTemplate,XMLExportImport: support python3

With these changes, we are able to install py2 business templates on py3,
but export is slightly different, because we already export using pickle
protocol 3 on py3.

To be able to install py2 business templates, we included heuristics to
guess the str or bytes from business template XML: oids are bytes and
also some strings that do not decode to UTF-8, so that we can install
python2 business templates on py3.

When exporting business templates, we need to build a list of referenced
persistent objects to export them separately in the XML, this is is done
using Unpickler.noload, in a way which does not support pickle protocol
1 on py3 (the persistent ids are None and the assertion in
https://github.com/zopefoundation/ZODB/blob/d698507bb89eeb38c6e655199bc9f54c909dbf4d/src/ZODB/serialize.py#L669
fails), so we need to use pickle protocol 3 on py3.

In the future, we might switch to exporting on protocol 3 on py2 as well
so that we have stable output on both py2 and py3, or maybe we'll do
this only when we stop supporting py2.
parent d99521e1
......@@ -29,7 +29,7 @@
import six
from six import string_types as basestring
from Products.ERP5Type.Utils import ensure_list, bytes2str
from Products.ERP5Type.Utils import ensure_list, bytes2str, str2bytes
import fnmatch, gc, glob, imp, os, re, shutil, sys, time, tarfile
from collections import defaultdict
from Shared.DC.ZRDB import Aqueduct
......@@ -79,6 +79,7 @@ from OFS import SimpleItem
from OFS.Image import Pdata
import coverage
from io import BytesIO
from six.moves import StringIO
from copy import deepcopy
from zExceptions import BadRequest
from Products.ERP5Type.XMLExportImport import exportXML, customImporters
......@@ -94,6 +95,10 @@ from importlib import import_module
import posixpath
import transaction
import inspect
if six.PY2:
BufferedReader = file
else:
from io import BufferedReader
import threading
from ZODB.broken import Broken, BrokenModified
......@@ -344,13 +349,17 @@ class BusinessTemplateArchive(object):
try:
write = self._writeFile
except AttributeError:
if not isinstance(obj, str):
if hasattr(obj, 'read'):
obj.seek(0)
obj = obj.read()
if not isinstance(obj, bytes):
obj = obj.encode('utf-8')
self.revision.hash(path, obj)
self._writeString(obj, path)
else:
if isinstance(obj, str):
obj = str2bytes(obj)
if isinstance(obj, bytes):
self.revision.hash(path, obj)
obj = BytesIO(obj)
else:
......@@ -372,11 +381,8 @@ class BusinessTemplateFolder(BusinessTemplateArchive):
object_path = os.path.join(self.path, path)
path = os.path.dirname(object_path)
os.path.exists(path) or os.makedirs(path)
f = open(object_path, 'wb')
try:
with open(object_path, 'wb') as f:
f.write(obj)
finally:
f.close()
def importFiles(self, item):
"""
......@@ -789,7 +795,10 @@ class ObjectTemplateItem(BaseTemplateItem):
return mime.extensions[0]
for ext in mime.globs:
if ext[0] == "*" and ext.count(".") == 1:
return ext[2:].encode("utf-8")
ext = ext[2:]
if six.PY2:
return ext.encode("utf-8")
return ext
# in case we could not read binary flag from mimetypes_registry then return
# '.bin' for all the Portal Types where exported_property_type is data
......@@ -833,8 +842,11 @@ class ObjectTemplateItem(BaseTemplateItem):
except (AttributeError, UnicodeEncodeError):
break
elif type(data) is not bytes:
if not isinstance(data, Pdata):
if isinstance(data, str):
data = data.encode()
elif not isinstance(data, Pdata):
break
else:
data = bytes(data)
try:
# Delete this attribute from the object.
......@@ -861,9 +873,9 @@ class ObjectTemplateItem(BaseTemplateItem):
obj = self.removeProperties(obj, 1, keep_workflow_history = True)
transaction.savepoint(optimistic=True)
f = BytesIO()
f = StringIO()
exportXML(obj._p_jar, obj._p_oid, f)
bta.addObject(f, key, path=path)
bta.addObject(str2bytes(f.getvalue()), key, path=path)
if catalog_method_template_item:
# add all datas specific to catalog inside one file
......@@ -917,7 +929,7 @@ class ObjectTemplateItem(BaseTemplateItem):
else:
connection = self.getConnection(self.aq_parent)
__traceback_info__ = 'Importing %s' % file_name
if hasattr(cache_database, 'db') and isinstance(file_obj, file):
if hasattr(cache_database, 'db') and isinstance(file_obj, BufferedReader):
obj = connection.importFile(self._compileXML(file_obj))
else:
# FIXME: Why not use the importXML function directly? Are there any BT5s
......@@ -1079,8 +1091,8 @@ class ObjectTemplateItem(BaseTemplateItem):
for path, old_object in upgrade_list:
# compare object to see it there is changes
new_object = self._objects[path]
new_io = BytesIO()
old_io = BytesIO()
new_io = StringIO()
old_io = StringIO()
exportXML(new_object._p_jar, new_object._p_oid, new_io)
new_obj_xml = new_io.getvalue()
try:
......@@ -1516,6 +1528,12 @@ class ObjectTemplateItem(BaseTemplateItem):
container.getParentValue().updateCache()
elif obj.__class__.__name__ in ('File', 'Image'):
if "data" in obj.__dict__:
# XXX when installing very old business templates without the data stored
# in a separate file (such as the one from
# testTemplateTool.TestTemplateTool.test_updateBusinessTemplateFromUrl_keep_list)
# data might be loaded as a string, fix this here.
if obj.data is not None and not isinstance(obj.data, (bytes, Pdata)):
obj.data = obj.data.encode()
# XXX Calling obj._setData() would call Interaction Workflow such
# as document_conversion_interaction_workflow which would update
# mime_type too...
......@@ -3504,14 +3522,14 @@ class PortalTypeRolesTemplateItem(BaseTemplateItem):
prop_value = role.get(property)
if prop_value:
if isinstance(prop_value, str):
prop_value = escape(prop_value.decode('utf-8'))
prop_value = escape(prop_value)
xml_data += "\n <property id='%s'>%s</property>" % \
(property, prop_value)
# multi
for property in ('categories', 'category', 'base_category'):
for prop_value in role.get(property, []):
if isinstance(prop_value, str):
prop_value = escape(prop_value.decode('utf-8'))
prop_value = escape(prop_value)
xml_data += "\n <multi_property "\
"id='%s'>%s</multi_property>" % (property, prop_value)
xml_data += "\n </role>"
......@@ -3524,7 +3542,7 @@ class PortalTypeRolesTemplateItem(BaseTemplateItem):
path = self.__class__.__name__
for key in self._objects:
xml_data = self.generateXml(key)
if isinstance(xml_data, six.text_type):
if six.PY2 and isinstance(xml_data, six.text_type):
xml_data = xml_data.encode('utf-8')
name = key.split('/', 1)[1]
bta.addObject(xml_data, name=name, path=path)
......@@ -3538,7 +3556,7 @@ class PortalTypeRolesTemplateItem(BaseTemplateItem):
xml_type_roles_list = xml.findall('role')
for role in xml_type_roles_list:
id = role.get('id')
if isinstance(id, six.text_type):
if six.PY2 and isinstance(id, six.text_type):
id = id.encode('utf_8', 'backslashreplace')
type_role_property_dict = {'id': id}
# uniq
......@@ -3547,7 +3565,7 @@ class PortalTypeRolesTemplateItem(BaseTemplateItem):
property_id = property_node.get('id')
if property_node.text:
value = property_node.text
if isinstance(value, six.text_type):
if six.PY2 and isinstance(value, six.text_type):
value = value.encode('utf_8', 'backslashreplace')
type_role_property_dict[property_id] = value
# multi
......@@ -3556,7 +3574,7 @@ class PortalTypeRolesTemplateItem(BaseTemplateItem):
property_id = property_node.get('id')
if property_node.text:
value = property_node.text
if isinstance(value, six.text_type):
if six.PY2 and isinstance(value, six.text_type):
value = value.encode('utf_8', 'backslashreplace')
type_role_property_dict.setdefault(property_id, []).append(value)
type_roles_list.append(type_role_property_dict)
......@@ -3964,7 +3982,7 @@ class FilesystemDocumentTemplateItem(BaseTemplateItem):
if not file_name.endswith('.py'):
LOG('Business Template', 0, 'Skipping file "%s"' % (file_name, ))
return
text = file.read()
text = file.read().decode('utf-8')
self._objects[file_name[:-3]] = text
class FilesystemToZodbTemplateItem(FilesystemDocumentTemplateItem,
......@@ -4965,7 +4983,7 @@ class LocalRolesTemplateItem(BaseTemplateItem):
xml_data += '\n </local_role_group_ids>'
xml_data += '\n</local_roles_item>'
if isinstance(xml_data, six.text_type):
if six.PY2 and isinstance(xml_data, six.text_type):
xml_data = xml_data.encode('utf8')
return xml_data
......@@ -6096,8 +6114,8 @@ Business Template is a set of definitions, such as skins, portal types and categ
'_test_item', '_message_translation_item',]
if item_name in item_list_1:
f1 = BytesIO() # for XML export of New Object
f2 = BytesIO() # For XML export of Installed Object
f1 = StringIO() # for XML export of New Object
f2 = StringIO() # For XML export of Installed Object
# Remove unneeded properties
new_object = new_item.removeProperties(new_object, 1)
installed_object = installed_item.removeProperties(installed_object, 1)
......@@ -6741,7 +6759,9 @@ Business Template is a set of definitions, such as skins, portal types and categ
from base64 import b64encode
def __newTempComponent(portal_type, reference, source_reference, migrate=False):
uid = b64encode("%s|%s|%s" % (portal_type, reference, source_reference))
uid = b64encode(("%s|%s|%s" % (portal_type, reference, source_reference)).encode())
if six.PY3:
uid = uid.decode()
if migrate:
bt_migratable_uid_list.append(uid)
......
......@@ -66,7 +66,7 @@ from Products.ERP5Type.Message import translateString
from zLOG import LOG, INFO, WARNING
import subprocess
import time
from Products.ERP5Type.Utils import bytes2str
from Products.ERP5Type.Utils import bytes2str, str2bytes, unicode2str
import json
WIN = os.name == 'nt'
......@@ -345,7 +345,9 @@ class TemplateTool (BaseTool):
try:
os.close(tempid) # Close the opened fd as soon as possible.
file_path, headers = urlretrieve(url, temppath)
if re.search(r'<title>.*Revision \d+:', open(file_path, 'r').read()):
with open(file_path, 'rb') as f:
content = f.read()
if re.search(br'<title>.*Revision \d+:', content):
# this looks like a subversion repository, try to check it out
LOG('ERP5', INFO, 'TemplateTool doing a svn checkout of %s' % url)
return self._download_svn(url, bt_id)
......@@ -712,7 +714,7 @@ class TemplateTool (BaseTool):
Return a repository and an id.
"""
repository, id = json.loads(b64decode(uid))
return repository.encode('utf-8'), id.encode('utf-8')
return unicode2str(repository), unicode2str(id)
security.declarePublic( 'encodeRepositoryBusinessTemplateUid' )
def encodeRepositoryBusinessTemplateUid(self, repository, id):
......@@ -720,7 +722,7 @@ class TemplateTool (BaseTool):
encode the repository and the id of a business template.
Return an uid.
"""
return b64encode(json.dumps((repository, id)))
return b64encode(str2bytes(json.dumps((repository, id))))
security.declarePublic('compareVersionStrings')
def compareVersionStrings(self, version, comparing_string):
......@@ -1066,7 +1068,7 @@ class TemplateTool (BaseTool):
installed_revision=installed_revision,
repository=repository,
**property_dict)
obj.setUid(uid)
obj.setUid(bytes2str(uid))
result_list.append(obj)
result_list.sort(key=lambda x: x.getTitle())
return result_list
......@@ -1113,7 +1115,7 @@ class TemplateTool (BaseTool):
e = int(e)
except ValueError:
# ASCII code is one byte, so this produces negative.
e = struct.unpack('b', e)[0] - 0x200
e = struct.unpack('b', e.encode())[0] - 0x200
except IndexError:
e = 0
return e
......
......@@ -40,19 +40,22 @@
from Acquisition import aq_base, aq_inner
from collections import OrderedDict
from io import BytesIO
from zodbpickle.pickle import Pickler
from zodbpickle.slowpickle import Pickler
from xml.sax.saxutils import escape, unescape
from lxml import etree
from lxml.etree import Element, SubElement
from xml_marshaller.xml_marshaller import Marshaller
from OFS.Image import Pdata
from base64 import standard_b64encode
import six
if six.PY2:
from base64 import standard_b64encode, encodestring as encodebytes
else:
from base64 import standard_b64encode, encodebytes
from hashlib import sha1
from Products.ERP5Type.Utils import ensure_list
from Products.ERP5Type.Utils import bytes2str
#from zLOG import LOG
import six
try:
long_ = long
except NameError: # six.PY3
......@@ -62,6 +65,9 @@ MARSHALLER_NAMESPACE_URI = 'http://www.erp5.org/namespaces/marshaller'
marshaller = Marshaller(namespace_uri=MARSHALLER_NAMESPACE_URI,
as_tree=True).dumps
DEFAULT_PICKLE_PROTOCOL = 1 if six.PY2 else 3
class OrderedPickler(Pickler):
"""Pickler producing consistent output by saving dicts in order
"""
......@@ -204,8 +210,8 @@ def Base_asXML(object, root=None):
local_group_node.append(marshaller(group_role[1]))
if return_as_object:
return root
return etree.tostring(root, encoding='utf-8',
xml_declaration=True, pretty_print=True)
return bytes2str(etree.tostring(root, encoding='utf-8',
xml_declaration=True, pretty_print=True))
def Folder_asXML(object, omit_xml_declaration=True, root=None):
"""
......@@ -226,24 +232,27 @@ def Folder_asXML(object, omit_xml_declaration=True, root=None):
if issubclass(o.__class__, Base):
o.asXML(root=root_node)
return etree.tostring(root, encoding='utf-8',
xml_declaration=xml_declaration, pretty_print=True)
return bytes2str(etree.tostring(root, encoding='utf-8',
xml_declaration=xml_declaration, pretty_print=True))
## The code below was initially from OFS.XMLExportImport
from six import string_types as basestring
from base64 import encodestring
from ZODB.serialize import referencesf
from ZODB.ExportImport import TemporaryFile, export_end_marker
from ZODB.utils import p64
from ZODB.utils import u64
from functools import partial
from inspect import getargspec
if six.PY2:
from inspect import getargspec as getfullargspec
else:
from inspect import getfullargspec
from OFS import ObjectManager
from . import ppml
magic=b'<?xm' # importXML(jar, file, clue)}
def reorderPickle(jar, p):
def reorderPickle(jar, p, pickle_protocol):
try:
from ZODB._compat import Unpickler, Pickler
except ImportError: # BBB: ZODB 3.10
......@@ -278,37 +287,47 @@ def reorderPickle(jar, p):
unpickler.persistent_load=persistent_load
newp=BytesIO()
pickler=OrderedPickler(newp,1)
pickler = OrderedPickler(newp, pickle_protocol)
pickler.persistent_id=persistent_id
classdef = unpickler.load()
obj = unpickler.load()
pickler.dump(classdef)
pickler.dump(obj)
if 0: # debug
debugp = BytesIO()
debugpickler = OrderedPickler(debugp, pickle_protocol)
debugpickler.persistent_id = persistent_id
debugpickler.dump(obj)
import pickletools
print(debugp.getvalue())
print(pickletools.dis(debugp.getvalue()))
p=newp.getvalue()
return obj, p
def _mapOid(id_mapping, oid):
idprefix = str(u64(oid))
id = id_mapping[idprefix]
old_aka = encodestring(oid)[:-1]
aka=encodestring(p64(long_(id)))[:-1] # Rebuild oid based on mapped id
old_aka = encodebytes(oid)[:-1]
aka=encodebytes(p64(long_(id)))[:-1] # Rebuild oid based on mapped id
id_mapping.setConvertedAka(old_aka, aka)
return idprefix+'.', id, aka
def XMLrecord(oid, plen, p, id_mapping):
# Proceed as usual
q=ppml.ToXMLUnpickler
f=BytesIO(p)
u=q(f)
f = BytesIO(p)
u = ppml.ToXMLUnpickler(f)
u.idprefix, id, aka = _mapOid(id_mapping, oid)
p=u.load(id_mapping=id_mapping).__str__(4)
p = u.load(id_mapping=id_mapping).__str__(4)
if f.tell() < plen:
p=p+u.load(id_mapping=id_mapping).__str__(4)
String=' <record id="%s" aka="%s">\n%s </record>\n' % (id, aka, p)
String=' <record id="%s" aka="%s">\n%s </record>\n' % (id, bytes2str(aka), p)
return String
def exportXML(jar, oid, file=None):
def exportXML(jar, oid, file=None, pickle_protocol=DEFAULT_PICKLE_PROTOCOL):
# For performance reasons, exportXML does not use 'XMLrecord' anymore to map
# oids. This requires to initialize MinimalMapping.marked_reference before
# any string output, i.e. in ppml.Reference.__init__
......@@ -316,15 +335,15 @@ def exportXML(jar, oid, file=None):
# can have values that have a shorter representation in 'repr' instead of
# 'base64' (see ppml.convert) and ppml.String does not support this.
load = jar._storage.load
if 'version' in getargspec(load).args: # BBB: ZODB<5 (TmpStore)
if 'version' in getfullargspec(load).args: # BBB: ZODB<5 (TmpStore)
load = partial(load, version='')
pickle_dict = {oid: None}
max_cache = [1e7] # do not cache more than 10MB of pickle data
def getReorderedPickle(oid):
p = pickle_dict[oid]
p = pickle_dict.get(oid)
if p is None:
p = load(oid)[0]
p = reorderPickle(jar, p)[1]
p = reorderPickle(jar, p, pickle_protocol)[1]
if len(p) < max_cache[0]:
max_cache[0] -= len(p)
pickle_dict[oid] = p
......@@ -342,9 +361,9 @@ def exportXML(jar, oid, file=None):
# Do real export
if file is None:
file = TemporaryFile()
file = TemporaryFile(mode='w')
elif isinstance(file, basestring):
file = open(file, 'w+b')
file = open(file, 'w')
write = file.write
write('<?xml version="1.0"?>\n<ZopeData>\n')
for oid in reordered_oid_list:
......@@ -403,7 +422,6 @@ def importXML(jar, file, clue=''):
F.end_handlers['record'] = save_record
F.end_handlers['ZopeData'] = save_zopedata
F.start_handlers['ZopeData'] = start_zopedata
F.binary=1
F.file=outfile
# <patch>
# Our BTs XML files don't declare encoding but have accented chars in them
......
This diff is collapsed.
......@@ -24,7 +24,7 @@ import string
import xml.parsers.expat
class xyap:
class xyap(object):
start_handlers = {}
end_handlers = {}
......@@ -57,7 +57,7 @@ class xyap:
top = end[tag](self, tag, top)
append(top)
class NoBlanks:
class NoBlanks(object):
def handle_data(self, data):
if data.strip():
......
......@@ -35,6 +35,7 @@ import glob
import os
import shutil
import tempfile
import warnings
from Acquisition import aq_base
from Testing import ZopeTestCase
......@@ -222,6 +223,11 @@ class CodingStyleTestCase(ERP5TypeTestCase):
if log_directory and diff_line_list:
with open(os.path.join(log_directory, '%s.diff' % self.id()), 'w') as f:
f.writelines(diff_line_list)
if diff_files and six.PY3: # TODO zope4py3
warnings.warn(
"Ignoring test_rebuild_business_template until we re-export "
"business templates with protocol 3.")
return
self.assertEqual(diff_files, [])
......
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