cubicweb/devtools/devctl.py
changeset 11726 a599e23c5712
parent 11716 c7de052ee288
child 11727 2efe0bf90ebb
equal deleted inserted replaced
11725:904ee9cd0cf9 11726:a599e23c5712
    18 """additional cubicweb-ctl commands and command handlers for cubicweb and
    18 """additional cubicweb-ctl commands and command handlers for cubicweb and
    19 cubicweb's cubes development
    19 cubicweb's cubes development
    20 """
    20 """
    21 from __future__ import print_function
    21 from __future__ import print_function
    22 
    22 
    23 __docformat__ = "restructuredtext en"
       
    24 
       
    25 # *ctl module should limit the number of import to be imported as quickly as
    23 # *ctl module should limit the number of import to be imported as quickly as
    26 # possible (for cubicweb-ctl reactivity, necessary for instance for usable bash
    24 # possible (for cubicweb-ctl reactivity, necessary for instance for usable bash
    27 # completion). So import locally in command helpers.
    25 # completion). So import locally in command helpers.
       
    26 
       
    27 import shutil
       
    28 import tempfile
    28 import sys
    29 import sys
    29 from datetime import datetime, date
    30 from datetime import datetime, date
    30 from os import mkdir, chdir, path as osp
    31 from os import mkdir, chdir, path as osp
    31 from warnings import warn
    32 from warnings import warn
    32 
    33 
    33 from pytz import UTC
    34 from pytz import UTC
    34 from six.moves import input
    35 from six.moves import input
    35 
    36 
    36 from logilab.common import STD_BLACKLIST
    37 from logilab.common import STD_BLACKLIST
       
    38 from logilab.common.fileutils import ensure_fs_mode
       
    39 from logilab.common.shellutils import find
    37 
    40 
    38 from cubicweb.__pkginfo__ import version as cubicwebversion
    41 from cubicweb.__pkginfo__ import version as cubicwebversion
    39 from cubicweb import CW_SOFTWARE_ROOT as BASEDIR, BadCommandUsage, ExecutionError
    42 from cubicweb import CW_SOFTWARE_ROOT as BASEDIR, BadCommandUsage, ExecutionError
       
    43 from cubicweb.i18n import extract_from_tal, execute2
    40 from cubicweb.cwctl import CWCTL
    44 from cubicweb.cwctl import CWCTL
    41 from cubicweb.cwconfig import CubicWebNoAppConfiguration
    45 from cubicweb.cwconfig import CubicWebNoAppConfiguration
    42 from cubicweb.toolsutils import (SKEL_EXCLUDE, Command, copy_skeleton,
    46 from cubicweb.toolsutils import (SKEL_EXCLUDE, Command, copy_skeleton,
    43                                  underline_title)
    47                                  underline_title)
    44 from cubicweb.web.webconfig import WebConfiguration
    48 from cubicweb.web.webconfig import WebConfiguration
    45 from cubicweb.server.serverconfig import ServerConfiguration
    49 from cubicweb.server.serverconfig import ServerConfiguration
       
    50 
       
    51 
       
    52 __docformat__ = "restructuredtext en"
    46 
    53 
    47 
    54 
    48 STD_BLACKLIST = set(STD_BLACKLIST)
    55 STD_BLACKLIST = set(STD_BLACKLIST)
    49 STD_BLACKLIST.add('.tox')
    56 STD_BLACKLIST.add('.tox')
    50 STD_BLACKLIST.add('test')
    57 STD_BLACKLIST.add('test')
   430                 print('* ' + '\n* '.join(toedit))
   437                 print('* ' + '\n* '.join(toedit))
   431                 print ('When you are done, run "cubicweb-ctl i18ninstance '
   438                 print ('When you are done, run "cubicweb-ctl i18ninstance '
   432                        '<yourinstance>" to see changes in your instances.')
   439                        '<yourinstance>" to see changes in your instances.')
   433             return True
   440             return True
   434 
   441 
       
   442 
       
   443 class I18nCubeMessageExtractor(object):
       
   444     """This class encapsulates all the xgettext extraction logic
       
   445 
       
   446     ``generate_pot_file`` is the main entry point called by the ``i18ncube``
       
   447     command. A cube might decide to customize extractors to ignore a given
       
   448     directory or to extract messages from a new file type (e.g. .jinja2 files)
       
   449 
       
   450     For each file type, the class must define two methods:
       
   451 
       
   452     - ``collect_{filetype}()`` that must return the list of files
       
   453       xgettext should inspect,
       
   454 
       
   455     - ``extract_{filetype}(files)`` that calls xgettext and returns the
       
   456       path to the generated ``pot`` file
       
   457     """
       
   458     blacklist = STD_BLACKLIST
       
   459     formats = ['tal', 'js', 'py']
       
   460 
       
   461     def __init__(self, workdir, cubedir):
       
   462         self.workdir = workdir
       
   463         self.cubedir = cubedir
       
   464 
       
   465     def generate_pot_file(self):
       
   466         """main entry point: return the generated ``cube.pot`` file
       
   467 
       
   468         This function first generates all the pot files (schema, tal,
       
   469         py, js) and then merges them in a single ``cube.pot`` that will
       
   470         be used to eventually update the ``i18n/*.po`` files.
       
   471         """
       
   472         potfiles = self.generate_pot_files()
       
   473         potfile = osp.join(self.workdir, 'cube.pot')
       
   474         print('-> merging %i .pot files' % len(potfiles))
       
   475         cmd = ['msgcat', '-o', potfile]
       
   476         cmd.extend(potfiles)
       
   477         execute2(cmd)
       
   478         return potfile if osp.exists(potfile) else None
       
   479 
       
   480     def find(self, exts, blacklist=None):
       
   481         """collect files with extensions ``exts`` in the cube directory
       
   482         """
       
   483         if blacklist is None:
       
   484             blacklist = self.blacklist
       
   485         return find(self.cubedir, exts, blacklist=blacklist)
       
   486 
       
   487     def generate_pot_files(self):
       
   488         """generate and return the list of all ``pot`` files for the cube
       
   489 
       
   490         - static-messages.pot,
       
   491         - schema.pot,
       
   492         - one ``pot`` file for each inspected format (.py, .js, etc.)
       
   493         """
       
   494         print('-> extracting messages:', end=' ')
       
   495         potfiles = []
       
   496         # static messages
       
   497         if osp.exists(osp.join('i18n', 'entities.pot')):
       
   498             warn('entities.pot is deprecated, rename file '
       
   499                  'to static-messages.pot (%s)'
       
   500                  % osp.join('i18n', 'entities.pot'), DeprecationWarning)
       
   501             potfiles.append(osp.join('i18n', 'entities.pot'))
       
   502         elif osp.exists(osp.join('i18n', 'static-messages.pot')):
       
   503             potfiles.append(osp.join('i18n', 'static-messages.pot'))
       
   504         # messages from schema
       
   505         potfiles.append(self.schemapot())
       
   506         # messages from sourcecode
       
   507         for fmt in self.formats:
       
   508             collector = getattr(self, 'collect_{0}'.format(fmt))
       
   509             extractor = getattr(self, 'extract_{0}'.format(fmt))
       
   510             files = collector()
       
   511             if files:
       
   512                 potfile = extractor(files)
       
   513                 if potfile:
       
   514                     potfiles.append(potfile)
       
   515         return potfiles
       
   516 
       
   517     def schemapot(self):
       
   518         """generate the ``schema.pot`` file"""
       
   519         schemapot = osp.join(self.workdir, 'schema.pot')
       
   520         print('schema', end=' ')
       
   521         # explicit close necessary else the file may not be yet flushed when
       
   522         # we'll using it below
       
   523         schemapotstream = open(schemapot, 'w')
       
   524         generate_schema_pot(schemapotstream.write, self.cubedir)
       
   525         schemapotstream.close()
       
   526         return schemapot
       
   527 
       
   528     def _xgettext(self, files, output, k='_', extraopts=''):
       
   529         """shortcut to execute the xgettext command and return output file
       
   530         """
       
   531         tmppotfile = osp.join(self.workdir, output)
       
   532         cmd = ['xgettext', '--no-location', '--omit-header', '-k' + k,
       
   533                '-o', tmppotfile] + extraopts.split() + files
       
   534         execute2(cmd)
       
   535         if osp.exists(tmppotfile):
       
   536             return tmppotfile
       
   537 
       
   538     def collect_tal(self):
       
   539         print('TAL', end=' ')
       
   540         return self.find(('.py', '.pt'))
       
   541 
       
   542     def extract_tal(self, files):
       
   543         tali18nfile = osp.join(self.workdir, 'tali18n.py')
       
   544         extract_from_tal(files, tali18nfile)
       
   545         return self._xgettext(files, output='tal.pot')
       
   546 
       
   547     def collect_js(self):
       
   548         print('Javascript')
       
   549         return [jsfile for jsfile in self.find('.js')
       
   550                 if osp.basename(jsfile).startswith('cub')]
       
   551 
       
   552     def extract_js(self, files):
       
   553         return self._xgettext(files, output='js.pot',
       
   554                               extraopts='-L java --from-code=utf-8')
       
   555 
       
   556     def collect_py(self):
       
   557         print('-> creating cube-specific catalog')
       
   558         return self.find('.py')
       
   559 
       
   560     def extract_py(self, files):
       
   561         return self._xgettext(files, output='py.pot')
       
   562 
       
   563 
   435 def update_cube_catalogs(cubedir):
   564 def update_cube_catalogs(cubedir):
   436     import shutil
   565     cubedir = osp.abspath(osp.normpath(cubedir))
   437     import tempfile
   566     workdir = tempfile.mkdtemp()
   438     from logilab.common.fileutils import ensure_fs_mode
   567     cube = osp.basename(cubedir)
   439     from logilab.common.shellutils import find, rm
   568     print('cubedir', cubedir)
   440     from cubicweb.i18n import extract_from_tal, execute2
       
   441     cube = osp.basename(osp.normpath(cubedir))
       
   442     tempdir = tempfile.mkdtemp()
       
   443     print(underline_title('Updating i18n catalogs for cube %s' % cube))
   569     print(underline_title('Updating i18n catalogs for cube %s' % cube))
   444     chdir(cubedir)
   570     chdir(cubedir)
   445     if osp.exists(osp.join('i18n', 'entities.pot')):
   571     extractor = I18nCubeMessageExtractor(workdir, cubedir)
   446         warn('entities.pot is deprecated, rename file to static-messages.pot (%s)'
   572     potfile = extractor.generate_pot_file()
   447              % osp.join('i18n', 'entities.pot'), DeprecationWarning)
   573     if potfile is None:
   448         potfiles = [osp.join('i18n', 'entities.pot')]
       
   449     elif osp.exists(osp.join('i18n', 'static-messages.pot')):
       
   450         potfiles = [osp.join('i18n', 'static-messages.pot')]
       
   451     else:
       
   452         potfiles = []
       
   453     print('-> extracting messages:', end=' ')
       
   454     print('schema', end=' ')
       
   455     schemapot = osp.join(tempdir, 'schema.pot')
       
   456     potfiles.append(schemapot)
       
   457     # explicit close necessary else the file may not be yet flushed when
       
   458     # we'll using it below
       
   459     schemapotstream = open(schemapot, 'w')
       
   460     generate_schema_pot(schemapotstream.write, cubedir)
       
   461     schemapotstream.close()
       
   462     print('TAL', end=' ')
       
   463     tali18nfile = osp.join(tempdir, 'tali18n.py')
       
   464     ptfiles = find('.', ('.py', '.pt'), blacklist=STD_BLACKLIST)
       
   465     extract_from_tal(ptfiles, tali18nfile)
       
   466     print('Javascript')
       
   467     jsfiles =  [jsfile for jsfile in find('.', '.js')
       
   468                 if osp.basename(jsfile).startswith('cub')]
       
   469     if jsfiles:
       
   470         tmppotfile = osp.join(tempdir, 'js.pot')
       
   471         cmd = ['xgettext', '--no-location', '--omit-header', '-k_', '-L', 'java',
       
   472                '--from-code=utf-8', '-o', tmppotfile] + jsfiles
       
   473         execute2(cmd)
       
   474         # no pot file created if there are no string to translate
       
   475         if osp.exists(tmppotfile):
       
   476             potfiles.append(tmppotfile)
       
   477     print('-> creating cube-specific catalog')
       
   478     tmppotfile = osp.join(tempdir, 'generated.pot')
       
   479     cubefiles = find('.', '.py', blacklist=STD_BLACKLIST)
       
   480     cubefiles.append(tali18nfile)
       
   481     cmd = ['xgettext', '--no-location', '--omit-header', '-k_', '-o', tmppotfile]
       
   482     cmd.extend(cubefiles)
       
   483     execute2(cmd)
       
   484     if osp.exists(tmppotfile): # doesn't exists of no translation string found
       
   485         potfiles.append(tmppotfile)
       
   486     potfile = osp.join(tempdir, 'cube.pot')
       
   487     print('-> merging %i .pot files' % len(potfiles))
       
   488     cmd = ['msgcat', '-o', potfile]
       
   489     cmd.extend(potfiles)
       
   490     execute2(cmd)
       
   491     if not osp.exists(potfile):
       
   492         print('no message catalog for cube', cube, 'nothing to translate')
   574         print('no message catalog for cube', cube, 'nothing to translate')
   493         # cleanup
   575         shutil.rmtree(workdir)
   494         rm(tempdir)
       
   495         return ()
   576         return ()
   496     print('-> merging main pot file with existing translations:', end=' ')
   577     print('-> merging main pot file with existing translations:', end=' ')
   497     chdir('i18n')
   578     chdir('i18n')
   498     toedit = []
   579     toedit = []
   499     for lang in CubicWebNoAppConfiguration.cw_languages():
   580     for lang in CubicWebNoAppConfiguration.cw_languages():
   500         print(lang, end=' ')
   581         print(lang, end=' ')
   501         cubepo = '%s.po' % lang
   582         cubepo = '%s.po' % lang
   502         if not osp.exists(cubepo):
   583         if not osp.exists(cubepo):
   503             shutil.copy(potfile, cubepo)
   584             shutil.copy(potfile, cubepo)
   504         else:
   585         else:
   505             cmd = ['msgmerge','-N','-s','-o', cubepo+'new', cubepo, potfile]
   586             cmd = ['msgmerge', '-N', '-s', '-o', cubepo + 'new',
       
   587                    cubepo, potfile]
   506             execute2(cmd)
   588             execute2(cmd)
   507             ensure_fs_mode(cubepo)
   589             ensure_fs_mode(cubepo)
   508             shutil.move('%snew' % cubepo, cubepo)
   590             shutil.move('%snew' % cubepo, cubepo)
   509         toedit.append(osp.abspath(cubepo))
   591         toedit.append(osp.abspath(cubepo))
   510     print()
   592     print()
   511     # cleanup
   593     # cleanup
   512     rm(tempdir)
   594     shutil.rmtree(workdir)
   513     return toedit
   595     return toedit
   514 
   596 
   515 
   597 
   516 class NewCubeCommand(Command):
   598 class NewCubeCommand(Command):
   517     """Create a new cube.
   599     """Create a new cube.