when reading the schema while adding/removing cubes, read schema in non-strict mode
"""utility to ease migration of application version to newly installedversion:organization: Logilab:copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr"""__docformat__="restructuredtext en"importsysimportosimportloggingfromtempfileimportmktempfromos.pathimportexists,join,basename,splitextfromlogilab.common.decoratorsimportcachedfromlogilab.common.configurationimportREQUIRED,read_old_configdefmigration_files(config,toupgrade):"""return an orderer list of path of scripts to execute to upgrade an installed application according to installed cube and cubicweb versions """merged=[]forcube,fromversion,toversionintoupgrade:ifcube=='cubicweb':migrdir=config.migration_scripts_dir()else:migrdir=config.cube_migration_scripts_dir(cube)scripts=filter_scripts(config,migrdir,fromversion,toversion)merged+=[s[1]forsinscripts]ifconfig.accept_mode('Any'):migrdir=config.migration_scripts_dir()merged.insert(0,join(migrdir,'bootstrapmigration_repository.py'))returnmergeddeffilter_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 """fromlogilab.common.changelogimportVersion# doesn't work with appengineassertfromversionasserttoversionassertisinstance(fromversion,tuple),fromversion.__class__assertisinstance(toversion,tuple),toversion.__class__assertfromversion<=toversion,(fromversion,toversion)ifnotexists(directory):ifnotquiet:printdirectory,"doesn't exists, no migration path"return[]iffromversion==toversion:return[]result=[]forfnameinos.listdir(directory):iffname.endswith('.pyc')orfname.endswith('.pyo') \orfname.endswith('~'):continuefpath=join(directory,fname)try:tver,mode=fname.split('_',1)exceptValueError:continuemode=mode.split('.',1)[0]ifnotconfig.accept_mode(mode):continuetry:tver=Version(tver)exceptValueError:continueiftver<=fromversion:continueiftver>toversion:continueresult.append((tver,fpath))# be sure scripts are executed in orderreturnsorted(result)IGNORED_EXTENSIONS=('.swp','~')defexecscript_confirm(scriptpath):"""asks for confirmation before executing a script and provides the ability to show the script's content """whileTrue:confirm=raw_input('** execute %r (Y/n/s[how]) ?'%scriptpath)confirm=confirm.strip().lower()ifconfirmin('n','no'):returnFalseelifconfirmin('s','show'):stream=open(scriptpath)scriptcontent=stream.read()stream.close()printprintscriptcontentprintelse:returnTruedefyes(*args,**kwargs):returnTrueclassMigrationHelper(object):"""class holding CubicWeb Migration Actions used by migration scripts"""def__init__(self,config,interactive=True,verbosity=1):self.config=configself.config.init_log(logthreshold=logging.ERROR,debug=True)# 0: no confirmation, 1: only main commands confirmed, 2 ask for everythingself.verbosity=verbosityself.need_wrap=Trueifnotinteractiveornotverbosity:self.confirm=yesself.execscript_confirm=yeselse:self.execscript_confirm=execscript_confirmself._option_changes=[]self.__context={'confirm':self.confirm,'config':self.config,'interactive_mode':interactive,}defrepo_connect(self):returnself.config.repository()defmigrate(self,vcconf,toupgrade,options):"""upgrade the given set of cubes `cubes` is an ordered list of 3-uple: (cube, fromversion, toversion) """ifoptions.fs_only:# monkey path configuration.accept_mode so database mode (e.g. Any)# won't be acceptedorig_accept_mode=self.config.accept_modedefaccept_mode(mode):ifmode=='Any':returnFalsereturnorig_accept_mode(mode)self.config.accept_mode=accept_modescripts=migration_files(self.config,toupgrade)ifscripts:vmap=dict((pname,(fromver,tover))forpname,fromver,toverintoupgrade)self.__context.update({'applcubicwebversion':vcconf['cubicweb'],'cubicwebversion':self.config.cubicweb_version(),'versions_map':vmap})self.scripts_session(scripts)else:print'no migration script to execute'defshutdown(self):passdef__getattribute__(self,name):try:returnobject.__getattribute__(self,name)exceptAttributeError:cmd='cmd_%s'%nameifhasattr(self,cmd):meth=getattr(self,cmd)returnlambda*args,**kwargs:self.interact(args,kwargs,meth=meth)raiseraiseAttributeError(name)definteract(self,args,kwargs,meth):"""execute the given method according to user's confirmation"""msg='execute command: %s(%s) ?'%(meth.__name__[4:],', '.join([repr(arg)forarginargs]+['%s=%r'%(n,v)forn,vinkwargs.items()]))if'ask_confirm'inkwargs:ask_confirm=kwargs.pop('ask_confirm')else:ask_confirm=Trueifnotask_confirmorself.confirm(msg):returnmeth(*args,**kwargs)defconfirm(self,question,shell=True,abort=True,retry=False):"""ask for confirmation and return true on positive answer if `retry` is true the r[etry] answer may return 2 """printquestion,possibleanswers='Y/n'ifabort:possibleanswers+='/a[bort]'ifshell:possibleanswers+='/s[hell]'ifretry:possibleanswers+='/r[etry]'try:confirm=raw_input('(%s): '%(possibleanswers,))answer=confirm.strip().lower()except(EOFError,KeyboardInterrupt):answer='abort'ifanswerin('n','no'):returnFalseifanswerin('r','retry'):return2ifanswerin('a','abort'):self.rollback()raiseSystemExit(1)ifshellandanswerin('s','shell'):self.interactive_shell()returnself.confirm(question)returnTruedefinteractive_shell(self):self.confirm=yesself.need_wrap=False# avoid '_' to be added to builtins by sys.display_hookdefdo_not_add___to_builtins(obj):ifobjisnotNone:printrepr(obj)sys.displayhook=do_not_add___to_builtinslocal_ctx=self._create_context()try:importreadlinefromrlcompleterimportCompleterexceptImportError:# readline not availablepasselse:readline.set_completer(Completer(local_ctx).complete)readline.parse_and_bind('tab: complete')histfile=os.path.join(os.environ["HOME"],".eshellhist")try:readline.read_history_file(histfile)exceptIOError:passfromcodeimportinteractbanner="""entering the migration python shelljust type migration commands or arbitrary python code and type ENTER to execute ittype "exit" or Ctrl-D to quit the shell and resume operation"""# give custom readfunc to avoid http://bugs.python.org/issue1288615defunicode_raw_input(prompt):returnunicode(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 questionsdelself.confirmself.need_wrap=True@cacheddef_create_context(self):"""return a dictionary to use as migration script execution context"""context=self.__contextforattrindir(self):ifattr.startswith('cmd_'):ifself.need_wrap:context[attr[4:]]=getattr(self,attr[4:])else:context[attr[4:]]=getattr(self,attr)returncontextdefprocess_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 """assertmigrscript.endswith('.py'),migrscriptifself.execscript_confirm(migrscript):scriptlocals=self._create_context().copy()iffuncnameisNone:pyname='__main__'else:pyname=splitext(basename(migrscript))[0]scriptlocals.update({'__file__':migrscript,'__name__':pyname})execfile(migrscript,scriptlocals)iffuncnameisnotNone:try:func=scriptlocals[funcname]self.info('found %s in locals',funcname)assertcallable(func),'%s (%s) is not callable'%(func,funcname)exceptKeyError:self.critical('no %s in script %s',funcname,migrscript)returnNonereturnfunc(*args,**kwargs)defscripts_session(self,migrscripts):"""execute some scripts in a transaction"""try:formigrscriptinmigrscripts:self.process_script(migrscript)self.commit()except:self.rollback()raisedefcmd_option_renamed(self,oldname,newname):"""a configuration option has been renamed"""self._option_changes.append(('renamed',oldname,newname))defcmd_option_group_change(self,option,oldgroup,newgroup):"""a configuration option has been moved in another group"""self._option_changes.append(('moved',option,oldgroup,newgroup))defcmd_option_added(self,optname):"""a configuration option has been added"""self._option_changes.append(('added',optname))defcmd_option_removed(self,optname):"""a configuration option has been removed"""# can safely be ignored#self._option_changes.append(('removed', optname))defcmd_option_type_changed(self,optname,oldtype,newvalue):"""a configuration option's type has changed"""self._option_changes.append(('typechanged',optname,oldtype,newvalue))defcmd_add_cubes(self,cubes):"""modify the list of used cubes in the in-memory config returns newly inserted cubes, including dependencies """ifisinstance(cubes,basestring):cubes=(cubes,)origcubes=self.config.cubes()newcubes=[pforpinself.config.expand_cubes(cubes)ifnotpinorigcubes]ifnewcubes:forcubeincubes:assertcubeinnewcubesself.config.add_cubes(newcubes)returnnewcubesdefcmd_remove_cube(self,cube):origcubes=self.config._cubesbasecubes=list(origcubes)forpkginself.config.expand_cubes([cube]):try:basecubes.remove(pkg)exceptValueError:continueself.config._cubes=tuple(self.config.expand_cubes(basecubes))removed=[pforpinorigcubesifnotpinself.config._cubes]assertcubeinremoved, \"can't remove cube %s, used as a dependancy"%cubereturnremoveddefrewrite_configuration(self):# import locally, show_diffs unavailable in gae environmentfromcubicweb.toolsutilsimportshow_diffsconfigfile=self.config.main_config_file()ifself._option_changes:read_old_config(self.config,self._option_changes,configfile)newconfig=mktemp()foroptdescrinself._option_changes:ifoptdescr[0]=='added':optdict=self.config.get_option_def(optdescr[1])ifoptdict.get('default')isREQUIRED:self.config.input_option(optdescr[1],optdict)self.config.generate_config(open(newconfig,'w'))show_diffs(configfile,newconfig)ifexists(newconfig):os.unlink(newconfig)fromloggingimportgetLoggerfromcubicwebimportset_log_methodsset_log_methods(MigrationHelper,getLogger('cubicweb.migration'))