diff --git a/src/cli/grampscli.py b/src/cli/grampscli.py index f664a11d6..ce318f58f 100644 --- a/src/cli/grampscli.py +++ b/src/cli/grampscli.py @@ -169,6 +169,12 @@ class CLIDbLoader(object): except gen.db.exceptions.DbUpgradeRequiredError, msg: self.dbstate.no_database() self._errordialog( _("Cannot open database"), str(msg)) + except gen.db.exceptions.PythonUpgradeRequiredError, msg: + self.dbstate.no_database() + self._errordialog( _("Cannot open database"), str(msg)) + except gen.db.exceptions.PythonDowngradeError, msg: + self.dbstate.no_database() + self._errordialog( _("Cannot open database"), str(msg)) except gen.db.exceptions.DbVersionError, msg: self.dbstate.no_database() self._errordialog( _("Cannot open database"), str(msg)) diff --git a/src/gen/db/exceptions.py b/src/gen/db/exceptions.py index 0bf4b6490..030618306 100644 --- a/src/gen/db/exceptions.py +++ b/src/gen/db/exceptions.py @@ -216,4 +216,60 @@ class DbUpgradeRequiredError(Exception): 'make a backup ' 'of your Family Tree.') % \ {'oldschema': self.oldschema, - 'newschema': self.newschema} \ No newline at end of file + 'newschema': self.newschema} + +class PythonDowngradeError(Exception): + """ + Error used to report that the Python version used to create the family tree + (i.e. Python3) is of a version that is newer than the current version + (i.e.Python2), so the Family Tree cannot be opened + """ + def __init__(self, db_python_version, current_python_version): + Exception.__init__(self) + self.db_python_version = str(db_python_version) + self.current_python_version = str(current_python_version) + + def __str__(self): + return _('The Family Tree you are trying to load was created with ' + 'Python version %(db_python_version)s. This version of Gramps ' + 'uses Python version %(current_python_version)s. So you are ' + 'trying to load ' + 'data created in a newer format into an older program, and ' + 'this is bound to fail.\n\n' + 'You should start your newer version of Gramps and ' + '' + 'make a backup of your Family Tree. You can then import ' + 'this backup into this version of Gramps.') % \ + {'db_python_version': self.db_python_version, + 'current_python_version': self.current_python_version} + +class PythonUpgradeRequiredError(Exception): + """ + Error used to report that the Python version used to create the family tree + (i.e. Python2) is earlier than the current Python version (i.e. Python3), so + the Family Tree needs to be upgraded.. + """ + def __init__(self, db_python_version, current_python_version): + Exception.__init__(self) + self.db_python_version = str(db_python_version) + self.current_python_version = str(current_python_version) + + def __str__(self): + return _('The Family Tree you are trying to load is in the Python ' + 'version %(db_python_version)s format. This version of Gramps ' + 'uses Python version %(current_python_version)s. Therefore ' + 'you cannot load this Family Tree without upgrading the ' + 'Python version of the Family Tree.\n\n' + 'If you upgrade then you won\'t be able to use the previous ' + 'version of Gramps, even if you subsequently ' + 'backup ' + 'or export ' + 'your upgraded Family Tree.\n\n' + 'Upgrading is a difficult task which could irretrievably ' + 'corrupt your Family Tree if it is interrupted or fails.\n\n' + 'If you have not already made a backup of your Family Tree, ' + 'then you should start your old version of Gramps and ' + 'make a backup ' + 'of your Family Tree.') % \ + {'db_python_version': self.db_python_version, + 'current_python_version': self.current_python_version} diff --git a/src/gen/db/write.py b/src/gen/db/write.py index 62da34456..20dba8f50 100644 --- a/src/gen/db/write.py +++ b/src/gen/db/write.py @@ -40,7 +40,7 @@ import locale import bisect from functools import wraps import logging -from sys import maxint, getfilesystemencoding +from sys import maxint, getfilesystemencoding, version_info from gen.ggettext import gettext as _ import config @@ -232,6 +232,7 @@ class DbBsddb(DbBsddbRead, DbWriteBase, UpdateCallback): self.has_changed = False self.brief_name = None self.update_env_version = False + self.update_python_version = False def catch_db_error(func): """ @@ -401,6 +402,7 @@ class DbBsddb(DbBsddbRead, DbWriteBase, UpdateCallback): else: # bsddb version is unknown env_version = "Unknown" +# _LOG.debug("db version %s, program version %s" % (bsddb_version, bdb_version)) if env_version == "Unknown" or \ (env_version[0] < bdb_version[0]) or \ @@ -451,6 +453,47 @@ class DbBsddb(DbBsddbRead, DbWriteBase, UpdateCallback): # This can't happen raise "Comparison between Bsddb version failed" + def __check_python_version(self, name, force_python_upgrade=False): + """ + The 'pickle' format (may) change with each Python version, see + http://docs.python.org/3.2/library/pickle.html#pickle. Code commits + 21777 and 21778 ensure that when going from python2 to python3, the old + format can be read. However, once the data has been written in the + python3 format, it will not be possible to go back to pyton2. This check + test whether we are changing python versions. If going from 2 to 3 it + warns the user, and allows it if he confirms. When going from 3 to 3, an + error is raised. Because code for python2 did not write the Python + version file, if the file is absent, python2 is assumed. + """ + current_python_version = version_info[0] + versionpath = os.path.join(self.path, "pythonversion.txt") + if os.path.isfile(versionpath): + with open(versionpath, "r") as version_file: + db_python_version = int(version_file.read().strip()) + else: + db_python_version = 2 + + if db_python_version == 3 and current_python_version == 2: + clear_lock_file(name) + raise exceptions.PythonDowngradeError(db_python_version, + current_python_version) + elif db_python_version == 2 and current_python_version > 2: + if not force_python_upgrade: + _LOG.debug("Python upgrade required from %s to %s" % + (db_python_version, current_python_version)) + clear_lock_file(name) + raise exceptions.PythonUpgradeRequiredError(db_python_version, + current_python_version) + # Try to do an upgrade + if not self.readonly: + _LOG.warning("Python upgrade requested from %s to %s" % + (db_python_version, current_python_version)) + self.update_python_version = True + # Make a backup of the database files anyway + self.__make_zip_backup(name) + elif db_python_version == 2 and current_python_version == 2: + pass + @catch_db_error def version_supported(self): dbversion = self.metadata.get('version', default=0) @@ -484,7 +527,8 @@ class DbBsddb(DbBsddbRead, DbWriteBase, UpdateCallback): @catch_db_error def load(self, name, callback, mode=DBMODE_W, force_schema_upgrade=False, - force_bsddb_upgrade=False, force_bsddb_downgrade=False): + force_bsddb_upgrade=False, force_bsddb_downgrade=False, + force_python_upgrade=False): if self.__check_readonly(name): mode = DBMODE_R @@ -504,8 +548,14 @@ class DbBsddb(DbBsddbRead, DbWriteBase, UpdateCallback): self.path = self.full_name self.brief_name = os.path.basename(name) - self.__check_bdb_version(name, force_bsddb_upgrade, - force_bsddb_downgrade) + # If we re-enter load with force_python_upgrade True, then we have + # already checked the bsddb version, and then checked python version, + # and are agreeing on the upgrade + if not force_python_upgrade: + self.__check_bdb_version(name, force_bsddb_upgrade, + force_bsddb_downgrade) + + self.__check_python_version(name, force_python_upgrade) # Set up database environment self.env = db.DBEnv() @@ -615,6 +665,12 @@ class DbBsddb(DbBsddbRead, DbWriteBase, UpdateCallback): version_file.write(str(db.version())) _LOG.debug("Updated BDBVERSFN file to %s" % str(db.version())) + if self.update_python_version: + versionpath = os.path.join(name, "pythonversion.txt") + with open(versionpath, "w") as version_file: + version_file.write(str(version_info[0])) + _LOG.debug("Updated python version file to %s" % str(version_info[0])) + # Here we take care of any changes in the tables related to new code. # If secondary indices change, then they should removed # or rebuilt by upgrade as well. In any case, the @@ -2062,6 +2118,11 @@ class DbBsddb(DbBsddbRead, DbWriteBase, UpdateCallback): with open(versionpath, "w") as version_file: version_file.write(str(db.version())) + versionpath = os.path.join(name, "pythonversion.txt") + _LOG.debug("Write python version file to %s" % str(version_info[0])) + with open(versionpath, "w") as version_file: + version_file.write(str(version_info[0])) + self.metadata.close() self.env.close() diff --git a/src/gui/dbloader.py b/src/gui/dbloader.py index da3cbfeee..b1512a330 100644 --- a/src/gui/dbloader.py +++ b/src/gui/dbloader.py @@ -298,13 +298,15 @@ class DbLoader(CLIDbLoader): force_schema_upgrade = False force_bsddb_upgrade = False force_bsddb_downgrade = False + force_python_upgrade = False try: while True: try: self.dbstate.db.load(filename, self._pulse_progress, mode, force_schema_upgrade, force_bsddb_upgrade, - force_bsddb_downgrade) + force_bsddb_downgrade, + force_python_upgrade) self.dbstate.db.set_save_path(filename) break except gen.db.exceptions.DbUpgradeRequiredError, msg: @@ -317,6 +319,7 @@ class DbLoader(CLIDbLoader): force_schema_upgrade = True force_bsddb_upgrade = False force_bsddb_downgrade = False + force_python_upgrade = False else: self.dbstate.no_database() break @@ -330,6 +333,7 @@ class DbLoader(CLIDbLoader): force_schema_upgrade = False force_bsddb_upgrade = True force_bsddb_downgrade = False + force_python_upgrade = False else: self.dbstate.no_database() break @@ -343,6 +347,21 @@ class DbLoader(CLIDbLoader): force_schema_upgrade = False force_bsddb_upgrade = False force_bsddb_downgrade = True + force_python_upgrade = False + else: + self.dbstate.no_database() + break + except gen.db.exceptions.PythonUpgradeRequiredError, msg: + if QuestionDialog2(_("Are you sure you want to upgrade " + "this Family Tree?"), + str(msg), + _("I have made a backup,\n" + "please upgrade my Family Tree"), + _("Cancel"), self.uistate.window).run(): + force_schema_upgrade = False + force_bsddb_upgrade = False + force_bsddb_downgrade = False + force_python_upgrade = True else: self.dbstate.no_database() break @@ -356,6 +375,9 @@ class DbLoader(CLIDbLoader): except gen.db.exceptions.DbEnvironmentError, msg: self.dbstate.no_database() self._errordialog( _("Cannot open database"), str(msg)) + except gen.db.exceptions.PythonDowngradeError, msg: + self.dbstate.no_database() + self._warn( _("Cannot open database"), str(msg)) except OSError, msg: self.dbstate.no_database() self._errordialog(