move i18n / migration modules from cw.common to cw
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Tue, 08 Dec 2009 10:40:12 +0100
changeset 4021 280c910c8710
parent 4018 d4d4e7112ccf
child 4022 934e758a73ef
move i18n / migration modules from cw.common to cw
common/i18n.py
common/migration.py
common/test/data/migration/0.0.3_Any.py
common/test/data/migration/0.0.4_Any.py
common/test/data/migration/0.1.0_Any.py
common/test/data/migration/0.1.0_common.py
common/test/data/migration/0.1.0_repository.py
common/test/data/migration/0.1.0_web.py
common/test/data/migration/0.1.2_Any.py
common/test/data/migration/depends.map
common/test/data/server_migration/2.10.2_Any.sql
common/test/data/server_migration/2.5.0_Any.sql
common/test/data/server_migration/2.6.0_Any.sql
common/test/data/server_migration/bootstrapmigration_repository.py
common/test/unittest_migration.py
cwconfig.py
cwctl.py
devtools/devctl.py
goa/goactl.py
i18n.py
migration.py
server/migractions.py
server/schemaserial.py
test/data/migration/0.0.3_Any.py
test/data/migration/0.0.4_Any.py
test/data/migration/0.1.0_Any.py
test/data/migration/0.1.0_common.py
test/data/migration/0.1.0_repository.py
test/data/migration/0.1.0_web.py
test/data/migration/0.1.2_Any.py
test/data/migration/depends.map
test/data/server_migration/2.10.2_Any.sql
test/data/server_migration/2.5.0_Any.sql
test/data/server_migration/2.6.0_Any.sql
test/data/server_migration/bootstrapmigration_repository.py
test/unittest_migration.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
--- 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'))
--- 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
--- 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
--- 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
--- 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
-"""
--- 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
-"""
--- 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
-"""
--- 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
--- 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
--- 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
-"""
--- 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()
--- 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')
--- 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)
--- 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()
--- 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)
--- /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
--- /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'))
--- 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
--- 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)
 
--- /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
--- /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
--- /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
--- /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
+"""
--- /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
+"""
--- /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
+"""
--- /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
--- /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
--- /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
+"""
--- /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()