comparison roundup/backends/rdbms_common.py @ 5525:bb7865241f8a

Make CSV import/export compatible across Python versions (also RDBMS journals) (issue 2550976, issue 2550975). The roundup-admin export and import commands are used for migrating between different database backends. It is desirable that they should be usable also for migrations between Python 2 and Python 3, and in some cases (e.g. with the anydbm backend) this may be required. To be usable for such migrations, the format of the generated CSV files needs to be stable, meaning the same as currently used with Python 2. The export process uses repr() to produce the fields in the CSV files and eval() to convert them back to Python data structures. repr() of strings with non-ASCII characters produces different results for Python 2 and Python 3. This patch adds repr_export and eval_import functions to roundup/anypy/strings.py which provide the required operations that are just repr() and eval() in Python 2, but are more complicated in Python 3 to use data representations compatible with Python 2. These functions are then used in the required places for export and import. repr() and eval() are also used in storing the dict of changed values in the journal for the RDBMS backends. It is similarly desirable that the database be compatible between Python 2 and Python 3, so that export and import do not need to be used for a migration between Python versions for non-anydbm back ends. Thus, this patch changes rdbms_common.py in the places involved in storing journals in the database, not just in those involved in import/export. Given this patch, import/export with non-ASCII characters appear based on some limited testing to work across Python versions, and an instance using the sqlite backend appears to be compatible between Python versions without needing import/export, *if* the sessions/otks databases (which use anydbm) are deleted when changing Python version.
author Joseph Myers <jsm@polyomino.org.uk>
date Sun, 02 Sep 2018 23:48:04 +0000
parents 6b0c542642be
children 1a0498c1ed90
comparison
equal deleted inserted replaced
5524:674ad58667b4 5525:bb7865241f8a
67 from roundup.backends.indexer_common import get_indexer 67 from roundup.backends.indexer_common import get_indexer
68 from roundup.backends.sessions_rdbms import Sessions, OneTimeKeys 68 from roundup.backends.sessions_rdbms import Sessions, OneTimeKeys
69 from roundup.date import Range 69 from roundup.date import Range
70 70
71 from roundup.backends.back_anydbm import compile_expression 71 from roundup.backends.back_anydbm import compile_expression
72 from roundup.anypy.strings import b2s, bs2b, us2s 72 from roundup.anypy.strings import b2s, bs2b, us2s, repr_export, eval_import
73 73
74 74
75 # dummy value meaning "argument not passed" 75 # dummy value meaning "argument not passed"
76 _marker = [] 76 _marker = []
77 77
1300 1300
1301 # make the journalled data marshallable 1301 # make the journalled data marshallable
1302 if isinstance(params, type({})): 1302 if isinstance(params, type({})):
1303 self._journal_marshal(params, classname) 1303 self._journal_marshal(params, classname)
1304 1304
1305 params = repr(params) 1305 params = repr_export(params)
1306 1306
1307 dc = self.to_sql_value(hyperdb.Date) 1307 dc = self.to_sql_value(hyperdb.Date)
1308 journaldate = dc(journaldate) 1308 journaldate = dc(journaldate)
1309 1309
1310 self.save_journal(classname, cols, nodeid, journaldate, 1310 self.save_journal(classname, cols, nodeid, journaldate,
1326 params)) 1326 params))
1327 1327
1328 # make the journalled data marshallable 1328 # make the journalled data marshallable
1329 if isinstance(params, type({})): 1329 if isinstance(params, type({})):
1330 self._journal_marshal(params, classname) 1330 self._journal_marshal(params, classname)
1331 params = repr(params) 1331 params = repr_export(params)
1332 1332
1333 self.save_journal(classname, cols, nodeid, dc(journaldate), 1333 self.save_journal(classname, cols, nodeid, dc(journaldate),
1334 journaltag, action, params) 1334 journaltag, action, params)
1335 1335
1336 def _journal_marshal(self, params, classname): 1336 def _journal_marshal(self, params, classname):
1364 # now unmarshal the data 1364 # now unmarshal the data
1365 dc = self.to_hyperdb_value(hyperdb.Date) 1365 dc = self.to_hyperdb_value(hyperdb.Date)
1366 res = [] 1366 res = []
1367 properties = self.getclass(classname).getprops() 1367 properties = self.getclass(classname).getprops()
1368 for nodeid, date_stamp, user, action, params in journal: 1368 for nodeid, date_stamp, user, action, params in journal:
1369 params = eval(params) 1369 params = eval_import(params)
1370 if isinstance(params, type({})): 1370 if isinstance(params, type({})):
1371 for param, value in params.items(): 1371 for param, value in params.items():
1372 if not value: 1372 if not value:
1373 continue 1373 continue
1374 property = properties.get(param, None) 1374 property = properties.get(param, None)
2890 value = value.get_tuple() 2890 value = value.get_tuple()
2891 elif isinstance(proptype, hyperdb.Interval): 2891 elif isinstance(proptype, hyperdb.Interval):
2892 value = value.get_tuple() 2892 value = value.get_tuple()
2893 elif isinstance(proptype, hyperdb.Password): 2893 elif isinstance(proptype, hyperdb.Password):
2894 value = str(value) 2894 value = str(value)
2895 l.append(repr(value)) 2895 l.append(repr_export(value))
2896 l.append(repr(self.is_retired(nodeid))) 2896 l.append(repr_export(self.is_retired(nodeid)))
2897 return l 2897 return l
2898 2898
2899 def import_list(self, propnames, proplist): 2899 def import_list(self, propnames, proplist):
2900 """ Import a node - all information including "id" is present and 2900 """ Import a node - all information including "id" is present and
2901 should not be sanity checked. Triggers are not triggered. The 2901 should not be sanity checked. Triggers are not triggered. The
2912 d = {} 2912 d = {}
2913 retire = 0 2913 retire = 0
2914 if not "id" in propnames: 2914 if not "id" in propnames:
2915 newid = self.db.newid(self.classname) 2915 newid = self.db.newid(self.classname)
2916 else: 2916 else:
2917 newid = eval(proplist[propnames.index("id")]) 2917 newid = eval_import(proplist[propnames.index("id")])
2918 for i in range(len(propnames)): 2918 for i in range(len(propnames)):
2919 # Use eval to reverse the repr() used to output the CSV 2919 # Use eval_import to reverse the repr_export() used to
2920 value = eval(proplist[i]) 2920 # output the CSV
2921 value = eval_import(proplist[i])
2921 2922
2922 # Figure the property for this column 2923 # Figure the property for this column
2923 propname = propnames[i] 2924 propname = propnames[i]
2924 2925
2925 # "unmarshal" where necessary 2926 # "unmarshal" where necessary
3008 params = export_data 3009 params = export_data
3009 elif action == 'create' and params: 3010 elif action == 'create' and params:
3010 # old tracker with data stored in the create! 3011 # old tracker with data stored in the create!
3011 params = {} 3012 params = {}
3012 l = [nodeid, date, user, action, params] 3013 l = [nodeid, date, user, action, params]
3013 r.append(list(map(repr, l))) 3014 r.append(list(map(repr_export, l)))
3014 return r 3015 return r
3015 3016
3016 class FileClass(hyperdb.FileClass, Class): 3017 class FileClass(hyperdb.FileClass, Class):
3017 """This class defines a large chunk of data. To support this, it has a 3018 """This class defines a large chunk of data. To support this, it has a
3018 mandatory String property "content" which is typically saved off 3019 mandatory String property "content" which is typically saved off

Roundup Issue Tracker: http://roundup-tracker.org/