# HG changeset patch # User Sylvain Thénault # Date 1260265212 -3600 # Node ID 280c910c87101495904d4db79f3cba3bcad9462c # Parent d4d4e7112ccf6ecea4d4860ab2d686bc1b458534 move i18n / migration modules from cw.common to cw diff -r d4d4e7112ccf -r 280c910c8710 common/i18n.py --- a/common/i18n.py Tue Dec 08 09:45:07 2009 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,99 +0,0 @@ -"""Some i18n/gettext utilities. - -:organization: Logilab -:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. -:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr -:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses -""" -__docformat__ = "restructuredtext en" - -import re -import os -import sys -from os.path import join, basename, splitext, exists -from glob import glob - -from cubicweb.toolsutils import create_dir - -def extract_from_tal(files, output_file): - """extract i18n strings from tal and write them into the given output file - using standard python gettext marker (_) - """ - output = open(output_file, 'w') - for filepath in files: - for match in re.finditer('i18n:(content|replace)="([^"]+)"', open(filepath).read()): - print >> output, '_("%s")' % match.group(2) - output.close() - - -def add_msg(w, msgid, msgctx=None): - """write an empty pot msgid definition""" - if isinstance(msgid, unicode): - msgid = msgid.encode('utf-8') - if msgctx: - if isinstance(msgctx, unicode): - msgctx = msgctx.encode('utf-8') - w('msgctxt "%s"\n' % msgctx) - msgid = msgid.replace('"', r'\"').splitlines() - if len(msgid) > 1: - w('msgid ""\n') - for line in msgid: - w('"%s"' % line.replace('"', r'\"')) - else: - w('msgid "%s"\n' % msgid[0]) - w('msgstr ""\n\n') - - -def execute(cmd): - """display the command, execute it and raise an Exception if returned - status != 0 - """ - from subprocess import call - print cmd.replace(os.getcwd() + os.sep, '') - status = call(cmd, shell=True) - if status != 0: - raise Exception('status = %s' % status) - - -def available_catalogs(i18ndir=None): - if i18ndir is None: - wildcard = '*.po' - else: - wildcard = join(i18ndir, '*.po') - for popath in glob(wildcard): - lang = splitext(basename(popath))[0] - yield lang, popath - - -def compile_i18n_catalogs(sourcedirs, destdir, langs): - """generate .mo files for a set of languages into the `destdir` i18n directory - """ - from logilab.common.fileutils import ensure_fs_mode - print '-> compiling %s catalogs...' % destdir - errors = [] - for lang in langs: - langdir = join(destdir, lang, 'LC_MESSAGES') - if not exists(langdir): - create_dir(langdir) - pofiles = [join(path, '%s.po' % lang) for path in sourcedirs] - pofiles = [pof for pof in pofiles if exists(pof)] - mergedpo = join(destdir, '%s_merged.po' % lang) - try: - # merge instance/cubes messages catalogs with the stdlib's one - execute('msgcat --use-first --sort-output --strict -o "%s" %s' - % (mergedpo, ' '.join('"%s"' % f for f in pofiles))) - # make sure the .mo file is writeable and compiles with *msgfmt* - applmo = join(destdir, lang, 'LC_MESSAGES', 'cubicweb.mo') - try: - ensure_fs_mode(applmo) - except OSError: - pass # suppose not exists - execute('msgfmt "%s" -o "%s"' % (mergedpo, applmo)) - except Exception, ex: - errors.append('while handling language %s: %s' % (lang, ex)) - try: - # clean everything - os.unlink(mergedpo) - except Exception: - continue - return errors diff -r d4d4e7112ccf -r 280c910c8710 common/migration.py --- a/common/migration.py Tue Dec 08 09:45:07 2009 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,373 +0,0 @@ -"""utilities for instances migration - -:organization: Logilab -:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. -:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr -:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses -""" -__docformat__ = "restructuredtext en" - -import sys -import os -import logging -import tempfile -from os.path import exists, join, basename, splitext - -from logilab.common.decorators import cached -from logilab.common.configuration import REQUIRED, read_old_config -from logilab.common.shellutils import ASK - -from cubicweb import ConfigurationError - - -def filter_scripts(config, directory, fromversion, toversion, quiet=True): - """return a list of paths of migration files to consider to upgrade - from a version to a greater one - """ - from logilab.common.changelog import Version # doesn't work with appengine - assert fromversion - assert toversion - assert isinstance(fromversion, tuple), fromversion.__class__ - assert isinstance(toversion, tuple), toversion.__class__ - assert fromversion <= toversion, (fromversion, toversion) - if not exists(directory): - if not quiet: - print directory, "doesn't exists, no migration path" - return [] - if fromversion == toversion: - return [] - result = [] - for fname in os.listdir(directory): - if fname.endswith('.pyc') or fname.endswith('.pyo') \ - or fname.endswith('~'): - continue - fpath = join(directory, fname) - try: - tver, mode = fname.split('_', 1) - except ValueError: - continue - mode = mode.split('.', 1)[0] - if not config.accept_mode(mode): - continue - try: - tver = Version(tver) - except ValueError: - continue - if tver <= fromversion: - continue - if tver > toversion: - continue - result.append((tver, fpath)) - # be sure scripts are executed in order - return sorted(result) - - -IGNORED_EXTENSIONS = ('.swp', '~') - - -def execscript_confirm(scriptpath): - """asks for confirmation before executing a script and provides the - ability to show the script's content - """ - while True: - answer = ASK.ask('Execute %r ?' % scriptpath, ('Y','n','show'), 'Y') - if answer == 'n': - return False - elif answer == 'show': - stream = open(scriptpath) - scriptcontent = stream.read() - stream.close() - print - print scriptcontent - print - else: - return True - -def yes(*args, **kwargs): - return True - - -class MigrationHelper(object): - """class holding CubicWeb Migration Actions used by migration scripts""" - - def __init__(self, config, interactive=True, verbosity=1): - self.config = config - if config: - # no config on shell to a remote instance - self.config.init_log(logthreshold=logging.ERROR, debug=True) - # 0: no confirmation, 1: only main commands confirmed, 2 ask for everything - self.verbosity = verbosity - self.need_wrap = True - if not interactive or not verbosity: - self.confirm = yes - self.execscript_confirm = yes - else: - self.execscript_confirm = execscript_confirm - self._option_changes = [] - self.__context = {'confirm': self.confirm, - 'config': self.config, - 'interactive_mode': interactive, - } - - def __getattribute__(self, name): - try: - return object.__getattribute__(self, name) - except AttributeError: - cmd = 'cmd_%s' % name - if hasattr(self, cmd): - meth = getattr(self, cmd) - return lambda *args, **kwargs: self.interact(args, kwargs, - meth=meth) - raise - raise AttributeError(name) - - def repo_connect(self): - return self.config.repository() - - def migrate(self, vcconf, toupgrade, options): - """upgrade the given set of cubes - - `cubes` is an ordered list of 3-uple: - (cube, fromversion, toversion) - """ - if options.fs_only: - # monkey path configuration.accept_mode so database mode (e.g. Any) - # won't be accepted - orig_accept_mode = self.config.accept_mode - def accept_mode(mode): - if mode == 'Any': - return False - return orig_accept_mode(mode) - self.config.accept_mode = accept_mode - # may be an iterator - toupgrade = tuple(toupgrade) - vmap = dict( (cube, (fromver, tover)) for cube, fromver, tover in toupgrade) - ctx = self.__context - ctx['versions_map'] = vmap - if self.config.accept_mode('Any') and 'cubicweb' in vmap: - migrdir = self.config.migration_scripts_dir() - self.cmd_process_script(join(migrdir, 'bootstrapmigration_repository.py')) - for cube, fromversion, toversion in toupgrade: - if cube == 'cubicweb': - migrdir = self.config.migration_scripts_dir() - else: - migrdir = self.config.cube_migration_scripts_dir(cube) - scripts = filter_scripts(self.config, migrdir, fromversion, toversion) - if scripts: - prevversion = None - for version, script in scripts: - # take care to X.Y.Z_Any.py / X.Y.Z_common.py: we've to call - # cube_upgraded once all script of X.Y.Z have been executed - if prevversion is not None and version != prevversion: - self.cube_upgraded(cube, prevversion) - prevversion = version - self.cmd_process_script(script) - self.cube_upgraded(cube, toversion) - else: - self.cube_upgraded(cube, toversion) - - def cube_upgraded(self, cube, version): - pass - - def shutdown(self): - pass - - def interact(self, args, kwargs, meth): - """execute the given method according to user's confirmation""" - msg = 'Execute command: %s(%s) ?' % ( - meth.__name__[4:], - ', '.join([repr(arg) for arg in args] + - ['%s=%r' % (n,v) for n,v in kwargs.items()])) - if 'ask_confirm' in kwargs: - ask_confirm = kwargs.pop('ask_confirm') - else: - ask_confirm = True - if not ask_confirm or self.confirm(msg): - return meth(*args, **kwargs) - - def confirm(self, question, shell=True, abort=True, retry=False, default='y'): - """ask for confirmation and return true on positive answer - - if `retry` is true the r[etry] answer may return 2 - """ - possibleanswers = ['y','n'] - if abort: - possibleanswers.append('abort') - if shell: - possibleanswers.append('shell') - if retry: - possibleanswers.append('retry') - try: - answer = ASK.ask(question, possibleanswers, default) - except (EOFError, KeyboardInterrupt): - answer = 'abort' - if answer == 'n': - return False - if answer == 'retry': - return 2 - if answer == 'abort': - raise SystemExit(1) - if shell and answer == 'shell': - self.interactive_shell() - return self.confirm(question) - return True - - def interactive_shell(self): - self.confirm = yes - self.need_wrap = False - # avoid '_' to be added to builtins by sys.display_hook - def do_not_add___to_builtins(obj): - if obj is not None: - print repr(obj) - sys.displayhook = do_not_add___to_builtins - local_ctx = self._create_context() - try: - import readline - from rlcompleter import Completer - except ImportError: - # readline not available - pass - else: - readline.set_completer(Completer(local_ctx).complete) - readline.parse_and_bind('tab: complete') - home_key = 'HOME' - if sys.platform == 'win32': - home_key = 'USERPROFILE' - histfile = os.path.join(os.environ[home_key], ".eshellhist") - try: - readline.read_history_file(histfile) - except IOError: - pass - from code import interact - banner = """entering the migration python shell -just type migration commands or arbitrary python code and type ENTER to execute it -type "exit" or Ctrl-D to quit the shell and resume operation""" - # give custom readfunc to avoid http://bugs.python.org/issue1288615 - def unicode_raw_input(prompt): - return unicode(raw_input(prompt), sys.stdin.encoding) - interact(banner, readfunc=unicode_raw_input, local=local_ctx) - readline.write_history_file(histfile) - # delete instance's confirm attribute to avoid questions - del self.confirm - self.need_wrap = True - - @cached - def _create_context(self): - """return a dictionary to use as migration script execution context""" - context = self.__context - for attr in dir(self): - if attr.startswith('cmd_'): - if self.need_wrap: - context[attr[4:]] = getattr(self, attr[4:]) - else: - context[attr[4:]] = getattr(self, attr) - return context - - def cmd_process_script(self, migrscript, funcname=None, *args, **kwargs): - """execute a migration script - in interactive mode, display the migration script path, ask for - confirmation and execute it if confirmed - """ - migrscript = os.path.normpath(migrscript) - if migrscript.endswith('.py'): - script_mode = 'python' - elif migrscript.endswith('.txt') or migrscript.endswith('.rst'): - script_mode = 'doctest' - else: - raise Exception('This is not a valid cubicweb shell input') - if not self.execscript_confirm(migrscript): - return - scriptlocals = self._create_context().copy() - if script_mode == 'python': - if funcname is None: - pyname = '__main__' - else: - pyname = splitext(basename(migrscript))[0] - scriptlocals.update({'__file__': migrscript, '__name__': pyname}) - execfile(migrscript, scriptlocals) - if funcname is not None: - try: - func = scriptlocals[funcname] - self.info('found %s in locals', funcname) - assert callable(func), '%s (%s) is not callable' % (func, funcname) - except KeyError: - self.critical('no %s in script %s', funcname, migrscript) - return None - return func(*args, **kwargs) - else: # script_mode == 'doctest' - import doctest - doctest.testfile(migrscript, module_relative=False, - optionflags=doctest.ELLIPSIS, globs=scriptlocals) - - def cmd_option_renamed(self, oldname, newname): - """a configuration option has been renamed""" - self._option_changes.append(('renamed', oldname, newname)) - - def cmd_option_group_change(self, option, oldgroup, newgroup): - """a configuration option has been moved in another group""" - self._option_changes.append(('moved', option, oldgroup, newgroup)) - - def cmd_option_added(self, optname): - """a configuration option has been added""" - self._option_changes.append(('added', optname)) - - def cmd_option_removed(self, optname): - """a configuration option has been removed""" - # can safely be ignored - #self._option_changes.append(('removed', optname)) - - def cmd_option_type_changed(self, optname, oldtype, newvalue): - """a configuration option's type has changed""" - self._option_changes.append(('typechanged', optname, oldtype, newvalue)) - - def cmd_add_cubes(self, cubes): - """modify the list of used cubes in the in-memory config - returns newly inserted cubes, including dependencies - """ - if isinstance(cubes, basestring): - cubes = (cubes,) - origcubes = self.config.cubes() - newcubes = [p for p in self.config.expand_cubes(cubes) - if not p in origcubes] - if newcubes: - for cube in cubes: - assert cube in newcubes - self.config.add_cubes(newcubes) - return newcubes - - def cmd_remove_cube(self, cube, removedeps=False): - if removedeps: - toremove = self.config.expand_cubes([cube]) - else: - toremove = (cube,) - origcubes = self.config._cubes - basecubes = [c for c in origcubes if not c in toremove] - self.config._cubes = tuple(self.config.expand_cubes(basecubes)) - removed = [p for p in origcubes if not p in self.config._cubes] - if not cube in removed: - raise ConfigurationError("can't remove cube %s, " - "used as a dependency" % cube) - return removed - - def rewrite_configuration(self): - # import locally, show_diffs unavailable in gae environment - from cubicweb.toolsutils import show_diffs - configfile = self.config.main_config_file() - if self._option_changes: - read_old_config(self.config, self._option_changes, configfile) - fd, newconfig = tempfile.mkstemp() - for optdescr in self._option_changes: - if optdescr[0] == 'added': - optdict = self.config.get_option_def(optdescr[1]) - if optdict.get('default') is REQUIRED: - self.config.input_option(optdescr[1], optdict) - self.config.generate_config(open(newconfig, 'w')) - show_diffs(configfile, newconfig) - os.close(fd) - if exists(newconfig): - os.unlink(newconfig) - - -from logging import getLogger -from cubicweb import set_log_methods -set_log_methods(MigrationHelper, getLogger('cubicweb.migration')) diff -r d4d4e7112ccf -r 280c910c8710 common/test/data/migration/0.0.3_Any.py --- a/common/test/data/migration/0.0.3_Any.py Tue Dec 08 09:45:07 2009 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,8 +0,0 @@ -""" - -:organization: Logilab -:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. -:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr -:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses -""" -coucou diff -r d4d4e7112ccf -r 280c910c8710 common/test/data/migration/0.0.4_Any.py --- a/common/test/data/migration/0.0.4_Any.py Tue Dec 08 09:45:07 2009 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,8 +0,0 @@ -""" - -:organization: Logilab -:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. -:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr -:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses -""" -coucou diff -r d4d4e7112ccf -r 280c910c8710 common/test/data/migration/0.1.0_Any.py --- a/common/test/data/migration/0.1.0_Any.py Tue Dec 08 09:45:07 2009 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,8 +0,0 @@ -""" - -:organization: Logilab -:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. -:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr -:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses -""" -coucou diff -r d4d4e7112ccf -r 280c910c8710 common/test/data/migration/0.1.0_common.py --- a/common/test/data/migration/0.1.0_common.py Tue Dec 08 09:45:07 2009 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,7 +0,0 @@ -"""common to all configuration - -:organization: Logilab -:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. -:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr -:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses -""" diff -r d4d4e7112ccf -r 280c910c8710 common/test/data/migration/0.1.0_repository.py --- a/common/test/data/migration/0.1.0_repository.py Tue Dec 08 09:45:07 2009 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,7 +0,0 @@ -"""repository specific - -:organization: Logilab -:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. -:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr -:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses -""" diff -r d4d4e7112ccf -r 280c910c8710 common/test/data/migration/0.1.0_web.py --- a/common/test/data/migration/0.1.0_web.py Tue Dec 08 09:45:07 2009 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,7 +0,0 @@ -"""web only - -:organization: Logilab -:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. -:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr -:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses -""" diff -r d4d4e7112ccf -r 280c910c8710 common/test/data/migration/0.1.2_Any.py --- a/common/test/data/migration/0.1.2_Any.py Tue Dec 08 09:45:07 2009 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,8 +0,0 @@ -""" - -:organization: Logilab -:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. -:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr -:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses -""" -coucou diff -r d4d4e7112ccf -r 280c910c8710 common/test/data/migration/depends.map --- a/common/test/data/migration/depends.map Tue Dec 08 09:45:07 2009 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,5 +0,0 @@ -0.0.2: 2.3.0 -0.0.3: 2.4.0 -# missing 0.0.4 entry, that's alright -0.1.0: 2.6.0 -0.1.2: 2.10.0 diff -r d4d4e7112ccf -r 280c910c8710 common/test/data/server_migration/2.10.2_Any.sql diff -r d4d4e7112ccf -r 280c910c8710 common/test/data/server_migration/2.5.0_Any.sql diff -r d4d4e7112ccf -r 280c910c8710 common/test/data/server_migration/2.6.0_Any.sql diff -r d4d4e7112ccf -r 280c910c8710 common/test/data/server_migration/bootstrapmigration_repository.py --- a/common/test/data/server_migration/bootstrapmigration_repository.py Tue Dec 08 09:45:07 2009 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,7 +0,0 @@ -"""allways executed before all others in server migration - -:organization: Logilab -:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. -:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr -:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses -""" diff -r d4d4e7112ccf -r 280c910c8710 common/test/unittest_migration.py --- a/common/test/unittest_migration.py Tue Dec 08 09:45:07 2009 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,103 +0,0 @@ -"""cubicweb.common.migration unit tests - -:organization: Logilab -:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. -:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr -:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses -""" - -from os.path import abspath -from logilab.common.testlib import TestCase, unittest_main - -from cubicweb.devtools import TestServerConfiguration -from cubicweb.cwconfig import CubicWebConfiguration -from cubicweb.common.migration import MigrationHelper, filter_scripts -from cubicweb.server.migractions import ServerMigrationHelper - - -class Schema(dict): - def has_entity(self, e_type): - return self.has_key(e_type) - -SMIGRDIR = abspath('data/server_migration') + '/' -TMIGRDIR = abspath('data/migration') + '/' - -class MigrTestConfig(TestServerConfiguration): - verbosity = 0 - def migration_scripts_dir(cls): - return SMIGRDIR - - def cube_migration_scripts_dir(cls, cube): - return TMIGRDIR - -class MigrationToolsTC(TestCase): - def setUp(self): - self.config = MigrTestConfig('data') - from yams.schema import Schema - self.config.load_schema = lambda expand_cubes=False: Schema('test') - self.config.__class__.cubicweb_appobject_path = frozenset() - self.config.__class__.cube_appobject_path = frozenset() - - def test_filter_scripts_base(self): - self.assertListEquals(filter_scripts(self.config, SMIGRDIR, (2,3,0), (2,4,0)), - []) - self.assertListEquals(filter_scripts(self.config, SMIGRDIR, (2,4,0), (2,5,0)), - [((2, 5, 0), SMIGRDIR+'2.5.0_Any.sql')]) - self.assertListEquals(filter_scripts(self.config, SMIGRDIR, (2,5,0), (2,6,0)), - [((2, 6, 0), SMIGRDIR+'2.6.0_Any.sql')]) - self.assertListEquals(filter_scripts(self.config, SMIGRDIR, (2,4,0), (2,6,0)), - [((2, 5, 0), SMIGRDIR+'2.5.0_Any.sql'), - ((2, 6, 0), SMIGRDIR+'2.6.0_Any.sql')]) - self.assertListEquals(filter_scripts(self.config, SMIGRDIR, (2,5,0), (2,5,1)), - []) - self.assertListEquals(filter_scripts(self.config, SMIGRDIR, (2,5,0), (2,10,2)), - [((2, 6, 0), SMIGRDIR+'2.6.0_Any.sql'), - ((2, 10, 2), SMIGRDIR+'2.10.2_Any.sql')]) - self.assertListEquals(filter_scripts(self.config, SMIGRDIR, (2,5,1), (2,6,0)), - [((2, 6, 0), SMIGRDIR+'2.6.0_Any.sql')]) - - self.assertListEquals(filter_scripts(self.config, TMIGRDIR, (0,0,2), (0,0,3)), - [((0, 0, 3), TMIGRDIR+'0.0.3_Any.py')]) - self.assertListEquals(filter_scripts(self.config, TMIGRDIR, (0,0,2), (0,0,4)), - [((0, 0, 3), TMIGRDIR+'0.0.3_Any.py'), - ((0, 0, 4), TMIGRDIR+'0.0.4_Any.py')]) - - def test_filter_scripts_for_mode(self): - config = CubicWebConfiguration('data') - config.verbosity = 0 - self.assert_(not isinstance(config.migration_handler(), ServerMigrationHelper)) - self.assertIsInstance(config.migration_handler(), MigrationHelper) - config = self.config - config.__class__.name = 'twisted' - self.assertListEquals(filter_scripts(config, TMIGRDIR, (0,0,4), (0,1,0)), - [((0, 1 ,0), TMIGRDIR+'0.1.0_common.py'), - ((0, 1 ,0), TMIGRDIR+'0.1.0_web.py')]) - config.__class__.name = 'repository' - self.assertListEquals(filter_scripts(config, TMIGRDIR, (0,0,4), (0,1,0)), - [((0, 1 ,0), TMIGRDIR+'0.1.0_Any.py'), - ((0, 1 ,0), TMIGRDIR+'0.1.0_common.py'), - ((0, 1 ,0), TMIGRDIR+'0.1.0_repository.py')]) - config.__class__.name = 'all-in-one' - self.assertListEquals(filter_scripts(config, TMIGRDIR, (0,0,4), (0,1,0)), - [((0, 1 ,0), TMIGRDIR+'0.1.0_Any.py'), - ((0, 1 ,0), TMIGRDIR+'0.1.0_common.py'), - ((0, 1 ,0), TMIGRDIR+'0.1.0_repository.py'), - ((0, 1 ,0), TMIGRDIR+'0.1.0_web.py')]) - config.__class__.name = 'repository' - - -from cubicweb.devtools import ApptestConfiguration, init_test_database, cleanup_sqlite - -class BaseCreationTC(TestCase): - - def test_db_creation(self): - """make sure database can be created""" - config = ApptestConfiguration('data') - source = config.sources()['system'] - self.assertEquals(source['db-driver'], 'sqlite') - cleanup_sqlite(source['db-name'], removetemplate=True) - init_test_database(config=config) - - -if __name__ == '__main__': - unittest_main() diff -r d4d4e7112ccf -r 280c910c8710 cwconfig.py --- a/cwconfig.py Tue Dec 08 09:45:07 2009 +0100 +++ b/cwconfig.py Tue Dec 08 10:40:12 2009 +0100 @@ -926,11 +926,11 @@ def migration_handler(self): """return a migration handler instance""" - from cubicweb.common.migration import MigrationHelper + from cubicweb.migration import MigrationHelper return MigrationHelper(self, verbosity=self.verbosity) def i18ncompile(self, langs=None): - from cubicweb.common import i18n + from cubicweb import i18n if langs is None: langs = self.available_languages() i18ndir = join(self.apphome, 'i18n') diff -r d4d4e7112ccf -r 280c910c8710 cwctl.py --- a/cwctl.py Tue Dec 08 09:45:07 2009 +0100 +++ b/cwctl.py Tue Dec 08 10:40:12 2009 +0100 @@ -311,7 +311,7 @@ # handle i18n files structure # in the first cube given print '-> preparing i18n catalogs' - from cubicweb.common import i18n + from cubicweb import i18n langs = [lang for lang, _ in i18n.available_catalogs(join(templdirs[0], 'i18n'))] errors = config.i18ncompile(langs) if errors: @@ -666,7 +666,7 @@ # * install new languages # * recompile catalogs # in the first componant given - from cubicweb.common import i18n + from cubicweb import i18n templdir = cwcfg.cube_dir(config.cubes()[0]) langs = [lang for lang, _ in i18n.available_catalogs(join(templdir, 'i18n'))] errors = config.i18ncompile(langs) diff -r d4d4e7112ccf -r 280c910c8710 devtools/devctl.py --- a/devtools/devctl.py Tue Dec 08 09:45:07 2009 +0100 +++ b/devtools/devctl.py Tue Dec 08 10:40:12 2009 +0100 @@ -113,7 +113,7 @@ def _generate_schema_pot(w, vreg, schema, libconfig=None, cube=None): - from cubicweb.common.i18n import add_msg + from cubicweb.i18n import add_msg from cubicweb.web import uicfg from cubicweb.schema import META_RTYPES, SYSTEM_RTYPES no_context_rtypes = META_RTYPES | SYSTEM_RTYPES @@ -286,7 +286,7 @@ import yams from logilab.common.fileutils import ensure_fs_mode from logilab.common.shellutils import globfind, find, rm - from cubicweb.common.i18n import extract_from_tal, execute + from cubicweb.i18n import extract_from_tal, execute tempdir = tempfile.mkdtemp() potfiles = [join(I18NDIR, 'static-messages.pot')] print '-> extract schema messages.' @@ -379,7 +379,7 @@ import tempfile from logilab.common.fileutils import ensure_fs_mode from logilab.common.shellutils import find, rm - from cubicweb.common.i18n import extract_from_tal, execute + from cubicweb.i18n import extract_from_tal, execute toedit = [] cube = basename(normpath(cubedir)) tempdir = tempfile.mkdtemp() diff -r d4d4e7112ccf -r 280c910c8710 goa/goactl.py --- a/goa/goactl.py Tue Dec 08 09:45:07 2009 +0100 +++ b/goa/goactl.py Tue Dec 08 10:40:12 2009 +0100 @@ -59,6 +59,8 @@ 'cwconfig.py', 'entity.py', 'interfaces.py', + 'i18n.py', + 'migration.py', 'rqlrewrite.py', 'rset.py', 'schema.py', @@ -69,7 +71,6 @@ 'view.py', 'common/mail.py', - 'common/migration.py', 'common/mixins.py', 'common/mttransforms.py', 'common/uilib.py', @@ -224,7 +225,7 @@ join(packagesdir, include)) # generate sample config from cubicweb.goa.goaconfig import GAEConfiguration - from cubicweb.common.migration import MigrationHelper + from cubicweb.migration import MigrationHelper config = GAEConfiguration(appid, appldir) if exists(config.main_config_file()): mih = MigrationHelper(config) diff -r d4d4e7112ccf -r 280c910c8710 i18n.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/i18n.py Tue Dec 08 10:40:12 2009 +0100 @@ -0,0 +1,99 @@ +"""Some i18n/gettext utilities. + +:organization: Logilab +:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. +:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr +:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses +""" +__docformat__ = "restructuredtext en" + +import re +import os +import sys +from os.path import join, basename, splitext, exists +from glob import glob + +from cubicweb.toolsutils import create_dir + +def extract_from_tal(files, output_file): + """extract i18n strings from tal and write them into the given output file + using standard python gettext marker (_) + """ + output = open(output_file, 'w') + for filepath in files: + for match in re.finditer('i18n:(content|replace)="([^"]+)"', open(filepath).read()): + print >> output, '_("%s")' % match.group(2) + output.close() + + +def add_msg(w, msgid, msgctx=None): + """write an empty pot msgid definition""" + if isinstance(msgid, unicode): + msgid = msgid.encode('utf-8') + if msgctx: + if isinstance(msgctx, unicode): + msgctx = msgctx.encode('utf-8') + w('msgctxt "%s"\n' % msgctx) + msgid = msgid.replace('"', r'\"').splitlines() + if len(msgid) > 1: + w('msgid ""\n') + for line in msgid: + w('"%s"' % line.replace('"', r'\"')) + else: + w('msgid "%s"\n' % msgid[0]) + w('msgstr ""\n\n') + + +def execute(cmd): + """display the command, execute it and raise an Exception if returned + status != 0 + """ + from subprocess import call + print cmd.replace(os.getcwd() + os.sep, '') + status = call(cmd, shell=True) + if status != 0: + raise Exception('status = %s' % status) + + +def available_catalogs(i18ndir=None): + if i18ndir is None: + wildcard = '*.po' + else: + wildcard = join(i18ndir, '*.po') + for popath in glob(wildcard): + lang = splitext(basename(popath))[0] + yield lang, popath + + +def compile_i18n_catalogs(sourcedirs, destdir, langs): + """generate .mo files for a set of languages into the `destdir` i18n directory + """ + from logilab.common.fileutils import ensure_fs_mode + print '-> compiling %s catalogs...' % destdir + errors = [] + for lang in langs: + langdir = join(destdir, lang, 'LC_MESSAGES') + if not exists(langdir): + create_dir(langdir) + pofiles = [join(path, '%s.po' % lang) for path in sourcedirs] + pofiles = [pof for pof in pofiles if exists(pof)] + mergedpo = join(destdir, '%s_merged.po' % lang) + try: + # merge instance/cubes messages catalogs with the stdlib's one + execute('msgcat --use-first --sort-output --strict -o "%s" %s' + % (mergedpo, ' '.join('"%s"' % f for f in pofiles))) + # make sure the .mo file is writeable and compiles with *msgfmt* + applmo = join(destdir, lang, 'LC_MESSAGES', 'cubicweb.mo') + try: + ensure_fs_mode(applmo) + except OSError: + pass # suppose not exists + execute('msgfmt "%s" -o "%s"' % (mergedpo, applmo)) + except Exception, ex: + errors.append('while handling language %s: %s' % (lang, ex)) + try: + # clean everything + os.unlink(mergedpo) + except Exception: + continue + return errors diff -r d4d4e7112ccf -r 280c910c8710 migration.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/migration.py Tue Dec 08 10:40:12 2009 +0100 @@ -0,0 +1,373 @@ +"""utilities for instances migration + +:organization: Logilab +:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. +:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr +:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses +""" +__docformat__ = "restructuredtext en" + +import sys +import os +import logging +import tempfile +from os.path import exists, join, basename, splitext + +from logilab.common.decorators import cached +from logilab.common.configuration import REQUIRED, read_old_config +from logilab.common.shellutils import ASK + +from cubicweb import ConfigurationError + + +def filter_scripts(config, directory, fromversion, toversion, quiet=True): + """return a list of paths of migration files to consider to upgrade + from a version to a greater one + """ + from logilab.common.changelog import Version # doesn't work with appengine + assert fromversion + assert toversion + assert isinstance(fromversion, tuple), fromversion.__class__ + assert isinstance(toversion, tuple), toversion.__class__ + assert fromversion <= toversion, (fromversion, toversion) + if not exists(directory): + if not quiet: + print directory, "doesn't exists, no migration path" + return [] + if fromversion == toversion: + return [] + result = [] + for fname in os.listdir(directory): + if fname.endswith('.pyc') or fname.endswith('.pyo') \ + or fname.endswith('~'): + continue + fpath = join(directory, fname) + try: + tver, mode = fname.split('_', 1) + except ValueError: + continue + mode = mode.split('.', 1)[0] + if not config.accept_mode(mode): + continue + try: + tver = Version(tver) + except ValueError: + continue + if tver <= fromversion: + continue + if tver > toversion: + continue + result.append((tver, fpath)) + # be sure scripts are executed in order + return sorted(result) + + +IGNORED_EXTENSIONS = ('.swp', '~') + + +def execscript_confirm(scriptpath): + """asks for confirmation before executing a script and provides the + ability to show the script's content + """ + while True: + answer = ASK.ask('Execute %r ?' % scriptpath, ('Y','n','show'), 'Y') + if answer == 'n': + return False + elif answer == 'show': + stream = open(scriptpath) + scriptcontent = stream.read() + stream.close() + print + print scriptcontent + print + else: + return True + +def yes(*args, **kwargs): + return True + + +class MigrationHelper(object): + """class holding CubicWeb Migration Actions used by migration scripts""" + + def __init__(self, config, interactive=True, verbosity=1): + self.config = config + if config: + # no config on shell to a remote instance + self.config.init_log(logthreshold=logging.ERROR, debug=True) + # 0: no confirmation, 1: only main commands confirmed, 2 ask for everything + self.verbosity = verbosity + self.need_wrap = True + if not interactive or not verbosity: + self.confirm = yes + self.execscript_confirm = yes + else: + self.execscript_confirm = execscript_confirm + self._option_changes = [] + self.__context = {'confirm': self.confirm, + 'config': self.config, + 'interactive_mode': interactive, + } + + def __getattribute__(self, name): + try: + return object.__getattribute__(self, name) + except AttributeError: + cmd = 'cmd_%s' % name + if hasattr(self, cmd): + meth = getattr(self, cmd) + return lambda *args, **kwargs: self.interact(args, kwargs, + meth=meth) + raise + raise AttributeError(name) + + def repo_connect(self): + return self.config.repository() + + def migrate(self, vcconf, toupgrade, options): + """upgrade the given set of cubes + + `cubes` is an ordered list of 3-uple: + (cube, fromversion, toversion) + """ + if options.fs_only: + # monkey path configuration.accept_mode so database mode (e.g. Any) + # won't be accepted + orig_accept_mode = self.config.accept_mode + def accept_mode(mode): + if mode == 'Any': + return False + return orig_accept_mode(mode) + self.config.accept_mode = accept_mode + # may be an iterator + toupgrade = tuple(toupgrade) + vmap = dict( (cube, (fromver, tover)) for cube, fromver, tover in toupgrade) + ctx = self.__context + ctx['versions_map'] = vmap + if self.config.accept_mode('Any') and 'cubicweb' in vmap: + migrdir = self.config.migration_scripts_dir() + self.cmd_process_script(join(migrdir, 'bootstrapmigration_repository.py')) + for cube, fromversion, toversion in toupgrade: + if cube == 'cubicweb': + migrdir = self.config.migration_scripts_dir() + else: + migrdir = self.config.cube_migration_scripts_dir(cube) + scripts = filter_scripts(self.config, migrdir, fromversion, toversion) + if scripts: + prevversion = None + for version, script in scripts: + # take care to X.Y.Z_Any.py / X.Y.Z_common.py: we've to call + # cube_upgraded once all script of X.Y.Z have been executed + if prevversion is not None and version != prevversion: + self.cube_upgraded(cube, prevversion) + prevversion = version + self.cmd_process_script(script) + self.cube_upgraded(cube, toversion) + else: + self.cube_upgraded(cube, toversion) + + def cube_upgraded(self, cube, version): + pass + + def shutdown(self): + pass + + def interact(self, args, kwargs, meth): + """execute the given method according to user's confirmation""" + msg = 'Execute command: %s(%s) ?' % ( + meth.__name__[4:], + ', '.join([repr(arg) for arg in args] + + ['%s=%r' % (n,v) for n,v in kwargs.items()])) + if 'ask_confirm' in kwargs: + ask_confirm = kwargs.pop('ask_confirm') + else: + ask_confirm = True + if not ask_confirm or self.confirm(msg): + return meth(*args, **kwargs) + + def confirm(self, question, shell=True, abort=True, retry=False, default='y'): + """ask for confirmation and return true on positive answer + + if `retry` is true the r[etry] answer may return 2 + """ + possibleanswers = ['y','n'] + if abort: + possibleanswers.append('abort') + if shell: + possibleanswers.append('shell') + if retry: + possibleanswers.append('retry') + try: + answer = ASK.ask(question, possibleanswers, default) + except (EOFError, KeyboardInterrupt): + answer = 'abort' + if answer == 'n': + return False + if answer == 'retry': + return 2 + if answer == 'abort': + raise SystemExit(1) + if shell and answer == 'shell': + self.interactive_shell() + return self.confirm(question) + return True + + def interactive_shell(self): + self.confirm = yes + self.need_wrap = False + # avoid '_' to be added to builtins by sys.display_hook + def do_not_add___to_builtins(obj): + if obj is not None: + print repr(obj) + sys.displayhook = do_not_add___to_builtins + local_ctx = self._create_context() + try: + import readline + from rlcompleter import Completer + except ImportError: + # readline not available + pass + else: + readline.set_completer(Completer(local_ctx).complete) + readline.parse_and_bind('tab: complete') + home_key = 'HOME' + if sys.platform == 'win32': + home_key = 'USERPROFILE' + histfile = os.path.join(os.environ[home_key], ".eshellhist") + try: + readline.read_history_file(histfile) + except IOError: + pass + from code import interact + banner = """entering the migration python shell +just type migration commands or arbitrary python code and type ENTER to execute it +type "exit" or Ctrl-D to quit the shell and resume operation""" + # give custom readfunc to avoid http://bugs.python.org/issue1288615 + def unicode_raw_input(prompt): + return unicode(raw_input(prompt), sys.stdin.encoding) + interact(banner, readfunc=unicode_raw_input, local=local_ctx) + readline.write_history_file(histfile) + # delete instance's confirm attribute to avoid questions + del self.confirm + self.need_wrap = True + + @cached + def _create_context(self): + """return a dictionary to use as migration script execution context""" + context = self.__context + for attr in dir(self): + if attr.startswith('cmd_'): + if self.need_wrap: + context[attr[4:]] = getattr(self, attr[4:]) + else: + context[attr[4:]] = getattr(self, attr) + return context + + def cmd_process_script(self, migrscript, funcname=None, *args, **kwargs): + """execute a migration script + in interactive mode, display the migration script path, ask for + confirmation and execute it if confirmed + """ + migrscript = os.path.normpath(migrscript) + if migrscript.endswith('.py'): + script_mode = 'python' + elif migrscript.endswith('.txt') or migrscript.endswith('.rst'): + script_mode = 'doctest' + else: + raise Exception('This is not a valid cubicweb shell input') + if not self.execscript_confirm(migrscript): + return + scriptlocals = self._create_context().copy() + if script_mode == 'python': + if funcname is None: + pyname = '__main__' + else: + pyname = splitext(basename(migrscript))[0] + scriptlocals.update({'__file__': migrscript, '__name__': pyname}) + execfile(migrscript, scriptlocals) + if funcname is not None: + try: + func = scriptlocals[funcname] + self.info('found %s in locals', funcname) + assert callable(func), '%s (%s) is not callable' % (func, funcname) + except KeyError: + self.critical('no %s in script %s', funcname, migrscript) + return None + return func(*args, **kwargs) + else: # script_mode == 'doctest' + import doctest + doctest.testfile(migrscript, module_relative=False, + optionflags=doctest.ELLIPSIS, globs=scriptlocals) + + def cmd_option_renamed(self, oldname, newname): + """a configuration option has been renamed""" + self._option_changes.append(('renamed', oldname, newname)) + + def cmd_option_group_change(self, option, oldgroup, newgroup): + """a configuration option has been moved in another group""" + self._option_changes.append(('moved', option, oldgroup, newgroup)) + + def cmd_option_added(self, optname): + """a configuration option has been added""" + self._option_changes.append(('added', optname)) + + def cmd_option_removed(self, optname): + """a configuration option has been removed""" + # can safely be ignored + #self._option_changes.append(('removed', optname)) + + def cmd_option_type_changed(self, optname, oldtype, newvalue): + """a configuration option's type has changed""" + self._option_changes.append(('typechanged', optname, oldtype, newvalue)) + + def cmd_add_cubes(self, cubes): + """modify the list of used cubes in the in-memory config + returns newly inserted cubes, including dependencies + """ + if isinstance(cubes, basestring): + cubes = (cubes,) + origcubes = self.config.cubes() + newcubes = [p for p in self.config.expand_cubes(cubes) + if not p in origcubes] + if newcubes: + for cube in cubes: + assert cube in newcubes + self.config.add_cubes(newcubes) + return newcubes + + def cmd_remove_cube(self, cube, removedeps=False): + if removedeps: + toremove = self.config.expand_cubes([cube]) + else: + toremove = (cube,) + origcubes = self.config._cubes + basecubes = [c for c in origcubes if not c in toremove] + self.config._cubes = tuple(self.config.expand_cubes(basecubes)) + removed = [p for p in origcubes if not p in self.config._cubes] + if not cube in removed: + raise ConfigurationError("can't remove cube %s, " + "used as a dependency" % cube) + return removed + + def rewrite_configuration(self): + # import locally, show_diffs unavailable in gae environment + from cubicweb.toolsutils import show_diffs + configfile = self.config.main_config_file() + if self._option_changes: + read_old_config(self.config, self._option_changes, configfile) + fd, newconfig = tempfile.mkstemp() + for optdescr in self._option_changes: + if optdescr[0] == 'added': + optdict = self.config.get_option_def(optdescr[1]) + if optdict.get('default') is REQUIRED: + self.config.input_option(optdescr[1], optdict) + self.config.generate_config(open(newconfig, 'w')) + show_diffs(configfile, newconfig) + os.close(fd) + if exists(newconfig): + os.unlink(newconfig) + + +from logging import getLogger +from cubicweb import set_log_methods +set_log_methods(MigrationHelper, getLogger('cubicweb.migration')) diff -r d4d4e7112ccf -r 280c910c8710 server/migractions.py --- a/server/migractions.py Tue Dec 08 09:45:07 2009 +0100 +++ b/server/migractions.py Tue Dec 08 10:40:12 2009 +0100 @@ -38,7 +38,7 @@ from cubicweb.schema import (META_RTYPES, VIRTUAL_RTYPES, CubicWebRelationSchema, order_eschemas) from cubicweb.dbapi import get_repository, repo_connect -from cubicweb.common.migration import MigrationHelper, yes +from cubicweb.migration import MigrationHelper, yes try: from cubicweb.server import SOURCE_TYPES, schemaserial as ss diff -r d4d4e7112ccf -r 280c910c8710 server/schemaserial.py --- a/server/schemaserial.py Tue Dec 08 09:45:07 2009 +0100 +++ b/server/schemaserial.py Tue Dec 08 10:40:12 2009 +0100 @@ -510,7 +510,7 @@ def rdefrelations2rql(rschema, subjtype, objtype, props): iterators = [] - for constraint in props['constraints']: + for constraint in props.constraints: iterators.append(constraint2rql(rschema, subjtype, objtype, constraint)) return chain(*iterators) diff -r d4d4e7112ccf -r 280c910c8710 test/data/migration/0.0.3_Any.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/data/migration/0.0.3_Any.py Tue Dec 08 10:40:12 2009 +0100 @@ -0,0 +1,8 @@ +""" + +:organization: Logilab +:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. +:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr +:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses +""" +coucou diff -r d4d4e7112ccf -r 280c910c8710 test/data/migration/0.0.4_Any.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/data/migration/0.0.4_Any.py Tue Dec 08 10:40:12 2009 +0100 @@ -0,0 +1,8 @@ +""" + +:organization: Logilab +:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. +:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr +:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses +""" +coucou diff -r d4d4e7112ccf -r 280c910c8710 test/data/migration/0.1.0_Any.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/data/migration/0.1.0_Any.py Tue Dec 08 10:40:12 2009 +0100 @@ -0,0 +1,8 @@ +""" + +:organization: Logilab +:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. +:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr +:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses +""" +coucou diff -r d4d4e7112ccf -r 280c910c8710 test/data/migration/0.1.0_common.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/data/migration/0.1.0_common.py Tue Dec 08 10:40:12 2009 +0100 @@ -0,0 +1,7 @@ +"""common to all configuration + +:organization: Logilab +:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. +:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr +:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses +""" diff -r d4d4e7112ccf -r 280c910c8710 test/data/migration/0.1.0_repository.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/data/migration/0.1.0_repository.py Tue Dec 08 10:40:12 2009 +0100 @@ -0,0 +1,7 @@ +"""repository specific + +:organization: Logilab +:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. +:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr +:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses +""" diff -r d4d4e7112ccf -r 280c910c8710 test/data/migration/0.1.0_web.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/data/migration/0.1.0_web.py Tue Dec 08 10:40:12 2009 +0100 @@ -0,0 +1,7 @@ +"""web only + +:organization: Logilab +:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. +:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr +:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses +""" diff -r d4d4e7112ccf -r 280c910c8710 test/data/migration/0.1.2_Any.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/data/migration/0.1.2_Any.py Tue Dec 08 10:40:12 2009 +0100 @@ -0,0 +1,8 @@ +""" + +:organization: Logilab +:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. +:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr +:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses +""" +coucou diff -r d4d4e7112ccf -r 280c910c8710 test/data/migration/depends.map --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/data/migration/depends.map Tue Dec 08 10:40:12 2009 +0100 @@ -0,0 +1,5 @@ +0.0.2: 2.3.0 +0.0.3: 2.4.0 +# missing 0.0.4 entry, that's alright +0.1.0: 2.6.0 +0.1.2: 2.10.0 diff -r d4d4e7112ccf -r 280c910c8710 test/data/server_migration/2.10.2_Any.sql diff -r d4d4e7112ccf -r 280c910c8710 test/data/server_migration/2.5.0_Any.sql diff -r d4d4e7112ccf -r 280c910c8710 test/data/server_migration/2.6.0_Any.sql diff -r d4d4e7112ccf -r 280c910c8710 test/data/server_migration/bootstrapmigration_repository.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/data/server_migration/bootstrapmigration_repository.py Tue Dec 08 10:40:12 2009 +0100 @@ -0,0 +1,7 @@ +"""allways executed before all others in server migration + +:organization: Logilab +:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. +:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr +:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses +""" diff -r d4d4e7112ccf -r 280c910c8710 test/unittest_migration.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/unittest_migration.py Tue Dec 08 10:40:12 2009 +0100 @@ -0,0 +1,103 @@ +"""cubicweb.common.migration unit tests + +:organization: Logilab +:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. +:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr +:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses +""" + +from os.path import abspath +from logilab.common.testlib import TestCase, unittest_main + +from cubicweb.devtools import TestServerConfiguration +from cubicweb.cwconfig import CubicWebConfiguration +from cubicweb.migration import MigrationHelper, filter_scripts +from cubicweb.server.migractions import ServerMigrationHelper + + +class Schema(dict): + def has_entity(self, e_type): + return self.has_key(e_type) + +SMIGRDIR = abspath('data/server_migration') + '/' +TMIGRDIR = abspath('data/migration') + '/' + +class MigrTestConfig(TestServerConfiguration): + verbosity = 0 + def migration_scripts_dir(cls): + return SMIGRDIR + + def cube_migration_scripts_dir(cls, cube): + return TMIGRDIR + +class MigrationToolsTC(TestCase): + def setUp(self): + self.config = MigrTestConfig('data') + from yams.schema import Schema + self.config.load_schema = lambda expand_cubes=False: Schema('test') + self.config.__class__.cubicweb_appobject_path = frozenset() + self.config.__class__.cube_appobject_path = frozenset() + + def test_filter_scripts_base(self): + self.assertListEquals(filter_scripts(self.config, SMIGRDIR, (2,3,0), (2,4,0)), + []) + self.assertListEquals(filter_scripts(self.config, SMIGRDIR, (2,4,0), (2,5,0)), + [((2, 5, 0), SMIGRDIR+'2.5.0_Any.sql')]) + self.assertListEquals(filter_scripts(self.config, SMIGRDIR, (2,5,0), (2,6,0)), + [((2, 6, 0), SMIGRDIR+'2.6.0_Any.sql')]) + self.assertListEquals(filter_scripts(self.config, SMIGRDIR, (2,4,0), (2,6,0)), + [((2, 5, 0), SMIGRDIR+'2.5.0_Any.sql'), + ((2, 6, 0), SMIGRDIR+'2.6.0_Any.sql')]) + self.assertListEquals(filter_scripts(self.config, SMIGRDIR, (2,5,0), (2,5,1)), + []) + self.assertListEquals(filter_scripts(self.config, SMIGRDIR, (2,5,0), (2,10,2)), + [((2, 6, 0), SMIGRDIR+'2.6.0_Any.sql'), + ((2, 10, 2), SMIGRDIR+'2.10.2_Any.sql')]) + self.assertListEquals(filter_scripts(self.config, SMIGRDIR, (2,5,1), (2,6,0)), + [((2, 6, 0), SMIGRDIR+'2.6.0_Any.sql')]) + + self.assertListEquals(filter_scripts(self.config, TMIGRDIR, (0,0,2), (0,0,3)), + [((0, 0, 3), TMIGRDIR+'0.0.3_Any.py')]) + self.assertListEquals(filter_scripts(self.config, TMIGRDIR, (0,0,2), (0,0,4)), + [((0, 0, 3), TMIGRDIR+'0.0.3_Any.py'), + ((0, 0, 4), TMIGRDIR+'0.0.4_Any.py')]) + + def test_filter_scripts_for_mode(self): + config = CubicWebConfiguration('data') + config.verbosity = 0 + self.assert_(not isinstance(config.migration_handler(), ServerMigrationHelper)) + self.assertIsInstance(config.migration_handler(), MigrationHelper) + config = self.config + config.__class__.name = 'twisted' + self.assertListEquals(filter_scripts(config, TMIGRDIR, (0,0,4), (0,1,0)), + [((0, 1 ,0), TMIGRDIR+'0.1.0_common.py'), + ((0, 1 ,0), TMIGRDIR+'0.1.0_web.py')]) + config.__class__.name = 'repository' + self.assertListEquals(filter_scripts(config, TMIGRDIR, (0,0,4), (0,1,0)), + [((0, 1 ,0), TMIGRDIR+'0.1.0_Any.py'), + ((0, 1 ,0), TMIGRDIR+'0.1.0_common.py'), + ((0, 1 ,0), TMIGRDIR+'0.1.0_repository.py')]) + config.__class__.name = 'all-in-one' + self.assertListEquals(filter_scripts(config, TMIGRDIR, (0,0,4), (0,1,0)), + [((0, 1 ,0), TMIGRDIR+'0.1.0_Any.py'), + ((0, 1 ,0), TMIGRDIR+'0.1.0_common.py'), + ((0, 1 ,0), TMIGRDIR+'0.1.0_repository.py'), + ((0, 1 ,0), TMIGRDIR+'0.1.0_web.py')]) + config.__class__.name = 'repository' + + +from cubicweb.devtools import ApptestConfiguration, init_test_database, cleanup_sqlite + +class BaseCreationTC(TestCase): + + def test_db_creation(self): + """make sure database can be created""" + config = ApptestConfiguration('data') + source = config.sources()['system'] + self.assertEquals(source['db-driver'], 'sqlite') + cleanup_sqlite(source['db-name'], removetemplate=True) + init_test_database(config=config) + + +if __name__ == '__main__': + unittest_main()