From 7fca4416c7f7ce66d528f0d1ad829cf3b8290b89 Mon Sep 17 00:00:00 2001 From: Ramiro Morales Date: Tue, 12 Feb 2013 16:50:47 -0300 Subject: [PATCH] Made (make|compile)messages check for availability of gettext commands. Refs #19584. --- .../management/commands/compilemessages.py | 7 +- .../core/management/commands/makemessages.py | 112 ++++++++++++------ django/core/management/utils.py | 40 ++++++- tests/i18n/commands/extraction.py | 9 +- tests/i18n/commands/tests.py | 26 +--- tests/user_commands/tests.py | 13 +- 6 files changed, 137 insertions(+), 70 deletions(-) diff --git a/django/core/management/commands/compilemessages.py b/django/core/management/commands/compilemessages.py index 2ca42d1c63..845ad159a9 100644 --- a/django/core/management/commands/compilemessages.py +++ b/django/core/management/commands/compilemessages.py @@ -5,7 +5,7 @@ import os from optparse import make_option from django.core.management.base import BaseCommand, CommandError -from django.core.management.utils import popen_wrapper +from django.core.management.utils import find_command, popen_wrapper from django.utils._os import npath def has_bom(fn): @@ -16,6 +16,10 @@ def has_bom(fn): sample.startswith(codecs.BOM_UTF16_BE) def compile_messages(stderr, locale=None): + program = 'msgfmt' + if find_command(program) is None: + raise CommandError("Can't find %s. Make sure you have GNU gettext tools 0.15 or newer installed." % program) + basedirs = [os.path.join('conf', 'locale'), 'locale'] if os.environ.get('DJANGO_SETTINGS_MODULE'): from django.conf import settings @@ -42,7 +46,6 @@ def compile_messages(stderr, locale=None): if has_bom(fn): raise CommandError("The %s file has a BOM (Byte Order Mark). Django only supports .po files encoded in UTF-8 and without any BOM." % fn) pf = os.path.splitext(fn)[0] - program = 'msgfmt' args = [program, '--check-format', '-o', npath(pf + '.mo'), npath(pf + '.po')] output, errors, status = popen_wrapper(args) if status: diff --git a/django/core/management/commands/makemessages.py b/django/core/management/commands/makemessages.py index e0b136a9e2..a7e98173ac 100644 --- a/django/core/management/commands/makemessages.py +++ b/django/core/management/commands/makemessages.py @@ -5,11 +5,11 @@ import re import sys from itertools import dropwhile from optparse import make_option -from subprocess import PIPE, Popen import django from django.core.management.base import CommandError, NoArgsCommand -from django.core.management.utils import handle_extensions +from django.core.management.utils import (handle_extensions, find_command, + popen_wrapper) from django.utils.functional import total_ordering from django.utils.text import get_text_list from django.utils.jslex import prepare_js_for_gettext @@ -18,6 +18,13 @@ plural_forms_re = re.compile(r'^(?P"Plural-Forms.+?\\n")\s*$', re.MULTILI STATUS_OK = 0 +def check_programs(*programs): + for program in programs: + if find_command(program) is None: + raise CommandError("Can't find %s. Make sure you have GNU " + "gettext tools 0.15 or newer installed." % program) + + @total_ordering class TranslatableFile(object): def __init__(self, dirpath, file_name): @@ -58,12 +65,24 @@ class TranslatableFile(object): work_file = os.path.join(self.dirpath, thefile) with open(work_file, "w") as fp: fp.write(src_data) - cmd = ( - 'xgettext -d %s -L C %s %s --keyword=gettext_noop ' - '--keyword=gettext_lazy --keyword=ngettext_lazy:1,2 ' - '--keyword=pgettext:1c,2 --keyword=npgettext:1c,2,3 ' - '--from-code UTF-8 --add-comments=Translators -o - "%s"' % - (domain, command.wrap, command.location, work_file)) + args = [ + 'xgettext', + '-d', domain, + '--language=C', + '--keyword=gettext_noop', + '--keyword=gettext_lazy', + '--keyword=ngettext_lazy:1,2', + '--keyword=pgettext:1c,2', + '--keyword=npgettext:1c,2,3', + '--from-code=UTF-8', + '--add-comments=Translators', + '--output=-' + ] + if command.wrap: + args.append(command.wrap) + if command.location: + args.append(command.location) + args.append(work_file) elif domain == 'django' and (file_ext == '.py' or file_ext in command.extensions): thefile = self.file orig_file = os.path.join(self.dirpath, self.file) @@ -76,18 +95,32 @@ class TranslatableFile(object): with open(os.path.join(self.dirpath, thefile), "w") as fp: fp.write(content) work_file = os.path.join(self.dirpath, thefile) - cmd = ( - 'xgettext -d %s -L Python %s %s --keyword=gettext_noop ' - '--keyword=gettext_lazy --keyword=ngettext_lazy:1,2 ' - '--keyword=ugettext_noop --keyword=ugettext_lazy ' - '--keyword=ungettext_lazy:1,2 --keyword=pgettext:1c,2 ' - '--keyword=npgettext:1c,2,3 --keyword=pgettext_lazy:1c,2 ' - '--keyword=npgettext_lazy:1c,2,3 --from-code UTF-8 ' - '--add-comments=Translators -o - "%s"' % - (domain, command.wrap, command.location, work_file)) + args = [ + 'xgettext', + '-d', domain, + '--language=Python', + '--keyword=gettext_noop', + '--keyword=gettext_lazy', + '--keyword=ngettext_lazy:1,2', + '--keyword=ugettext_noop', + '--keyword=ugettext_lazy', + '--keyword=ungettext_lazy:1,2', + '--keyword=pgettext:1c,2', + '--keyword=npgettext:1c,2,3', + '--keyword=pgettext_lazy:1c,2', + '--keyword=npgettext_lazy:1c,2,3', + '--from-code=UTF-8', + '--add-comments=Translators', + '--output=-' + ] + if command.wrap: + args.append(command.wrap) + if command.location: + args.append(command.location) + args.append(work_file) else: return - msgs, errors, status = _popen(cmd) + msgs, errors, status = popen_wrapper(args) if errors: if status != STATUS_OK: if is_templatized: @@ -109,15 +142,6 @@ class TranslatableFile(object): if is_templatized: os.unlink(work_file) - -def _popen(cmd): - """ - Friendly wrapper around Popen for Windows - """ - p = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE, close_fds=os.name != 'nt', universal_newlines=True) - output, errors = p.communicate() - return output, errors, p.returncode - def write_pot_file(potfile, msgs): """ Write the :param potfile: POT file with the :param msgs: contents, @@ -225,8 +249,9 @@ class Command(NoArgsCommand): "is not created automatically, you have to create it by hand " "if you want to enable i18n for your project or application.") + check_programs('xgettext') # We require gettext version 0.15 or newer. - output, errors, status = _popen('xgettext --version') + output, errors, status = popen_wrapper(['xgettext', '--version']) if status != STATUS_OK: raise CommandError("Error running xgettext. Note that Django " "internationalization requires GNU gettext 0.15 or newer.") @@ -248,6 +273,9 @@ class Command(NoArgsCommand): locale_dirs = filter(os.path.isdir, glob.glob('%s/*' % localedir)) locales = [os.path.basename(l) for l in locale_dirs] + if locales: + check_programs('msguniq', 'msgmerge', 'msgattrib') + try: for locale in locales: if self.verbosity > 0: @@ -307,8 +335,13 @@ class Command(NoArgsCommand): Uses mguniq, msgmerge, and msgattrib GNU gettext utilities. """ - msgs, errors, status = _popen('msguniq %s %s --to-code=utf-8 "%s"' % - (self.wrap, self.location, potfile)) + args = ['msguniq', '--to-code=utf-8'] + if self.wrap: + args.append(self.wrap) + if self.location: + args.append(self.location) + args.append(potfile) + msgs, errors, status = popen_wrapper(args) if errors: if status != STATUS_OK: raise CommandError( @@ -324,8 +357,13 @@ class Command(NoArgsCommand): if os.path.exists(pofile): with open(potfile, 'w') as fp: fp.write(msgs) - msgs, errors, status = _popen('msgmerge %s %s -q "%s" "%s"' % - (self.wrap, self.location, pofile, potfile)) + args = ['msgmerge', '-q'] + if self.wrap: + args.append(self.wrap) + if self.location: + args.append(self.location) + args.extend([pofile, potfile]) + msgs, errors, status = popen_wrapper(args) if errors: if status != STATUS_OK: raise CommandError( @@ -340,9 +378,13 @@ class Command(NoArgsCommand): fp.write(msgs) if self.no_obsolete: - msgs, errors, status = _popen( - 'msgattrib %s %s -o "%s" --no-obsolete "%s"' % - (self.wrap, self.location, pofile, pofile)) + args = ['msgattrib', '-o', pofile, '--no-obsolete'] + if self.wrap: + args.append(self.wrap) + if self.location: + args.append(self.location) + args.append(pofile) + msgs, errors, status = popen_wrapper(args) if errors: if status != STATUS_OK: raise CommandError( diff --git a/django/core/management/utils.py b/django/core/management/utils.py index de6e57f097..7159d1123f 100644 --- a/django/core/management/utils.py +++ b/django/core/management/utils.py @@ -1,17 +1,27 @@ +from __future__ import absolute_import + import os from subprocess import PIPE, Popen +import sys from django.utils.encoding import force_text, DEFAULT_LOCALE_ENCODING +from django.utils import six + +from .base import CommandError -def popen_wrapper(args): +def popen_wrapper(args, os_err_exc_type=CommandError): """ Friendly wrapper around Popen. Returns stdout output, stderr output and OS status code. """ - p = Popen(args, shell=False, stdout=PIPE, stderr=PIPE, - close_fds=os.name != 'nt', universal_newlines=True) + try: + p = Popen(args, shell=False, stdout=PIPE, stderr=PIPE, + close_fds=os.name != 'nt', universal_newlines=True) + except OSError as e: + six.reraise(os_err_exc_type, os_err_exc_type('Error executing %s: %s' % + (args[0], e.strerror)), sys.exc_info()[2]) output, errors = p.communicate() return ( output, @@ -43,3 +53,27 @@ def handle_extensions(extensions=('html',), ignored=('py',)): if not ext.startswith('.'): ext_list[i] = '.%s' % ext_list[i] return set([x for x in ext_list if x.strip('.') not in ignored]) + +def find_command(cmd, path=None, pathext=None): + if path is None: + path = os.environ.get('PATH', []).split(os.pathsep) + if isinstance(path, six.string_types): + path = [path] + # check if there are funny path extensions for executables, e.g. Windows + if pathext is None: + pathext = os.environ.get('PATHEXT', '.COM;.EXE;.BAT;.CMD').split(os.pathsep) + # don't use extensions if the command ends with one of them + for ext in pathext: + if cmd.endswith(ext): + pathext = [''] + break + # check if we find the command on PATH + for p in path: + f = os.path.join(p, cmd) + if os.path.isfile(f): + return f + for ext in pathext: + fext = f + ext + if os.path.isfile(fext): + return fext + return None diff --git a/tests/i18n/commands/extraction.py b/tests/i18n/commands/extraction.py index ca89f30de8..80e4ee0110 100644 --- a/tests/i18n/commands/extraction.py +++ b/tests/i18n/commands/extraction.py @@ -131,8 +131,13 @@ class BasicExtractorTests(ExtractorTests): os.chdir(self.test_dir) shutil.copyfile('./code.sample', './code_sample.py') stdout = StringIO() - management.call_command('makemessages', locale=LOCALE, stdout=stdout) - os.remove('./code_sample.py') + try: + management.call_command('makemessages', locale=LOCALE, stdout=stdout) + finally: + try: + os.remove('./code_sample.py') + except OSError: + pass self.assertIn("code_sample.py:4", force_text(stdout.getvalue())) def test_template_message_context_extractor(self): diff --git a/tests/i18n/commands/tests.py b/tests/i18n/commands/tests.py index e00ef72d59..f9e3c20fff 100644 --- a/tests/i18n/commands/tests.py +++ b/tests/i18n/commands/tests.py @@ -2,35 +2,11 @@ import os import re from subprocess import Popen, PIPE -from django.utils import six +from django.core.management.utils import find_command can_run_extraction_tests = False can_run_compilation_tests = False -def find_command(cmd, path=None, pathext=None): - if path is None: - path = os.environ.get('PATH', []).split(os.pathsep) - if isinstance(path, six.string_types): - path = [path] - # check if there are funny path extensions for executables, e.g. Windows - if pathext is None: - pathext = os.environ.get('PATHEXT', '.COM;.EXE;.BAT;.CMD').split(os.pathsep) - # don't use extensions if the command ends with one of them - for ext in pathext: - if cmd.endswith(ext): - pathext = [''] - break - # check if we find the command on PATH - for p in path: - f = os.path.join(p, cmd) - if os.path.isfile(f): - return f - for ext in pathext: - fext = f + ext - if os.path.isfile(fext): - return fext - return None - # checks if it can find xgettext on the PATH and # imports the extraction tests if yes xgettext_cmd = find_command('xgettext') diff --git a/tests/user_commands/tests.py b/tests/user_commands/tests.py index c8740577a5..bf555e66b9 100644 --- a/tests/user_commands/tests.py +++ b/tests/user_commands/tests.py @@ -1,13 +1,14 @@ import sys from django.core import management -from django.core.management.base import CommandError -from django.test import TestCase +from django.core.management import CommandError +from django.core.management.utils import popen_wrapper +from django.test import SimpleTestCase from django.utils import translation from django.utils.six import StringIO -class CommandTests(TestCase): +class CommandTests(SimpleTestCase): def test_command(self): out = StringIO() management.call_command('dance', stdout=out) @@ -58,3 +59,9 @@ class CommandTests(TestCase): with translation.override('pl'): management.call_command('leave_locale_alone_true', stdout=out) self.assertEqual(out.getvalue(), "pl\n") + + +class UtilsTests(SimpleTestCase): + + def test_no_existent_external_program(self): + self.assertRaises(CommandError, popen_wrapper, ['a_42_command_that_doesnt_exist_42'])