test_extraction.py 30.5 KB
Newer Older
1
# -*- encoding: utf-8 -*-
2
from __future__ import unicode_literals
3

4
import io
5 6 7
import os
import re
import shutil
8
import sys
9
import time
10
from unittest import SkipTest, skipUnless
11
import warnings
12

13
from django.conf import settings
14
from django.core import management
15
from django.core.management import execute_from_command_line
16
from django.core.management.utils import find_command
17
from django.test import SimpleTestCase
18
from django.test import override_settings
19 20
from django.utils.encoding import force_text
from django.utils._os import upath
21
from django.utils import six
22
from django.utils.six import StringIO
23
from django.utils.translation import TranslatorCommentWarning
24

25

Tim Graham's avatar
Tim Graham committed
26
LOCALE = 'de'
27
has_xgettext = find_command('xgettext')
28
this_directory = os.path.dirname(upath(__file__))
29

Jason Myers's avatar
Jason Myers committed
30

31
@skipUnless(has_xgettext, 'xgettext is mandatory for extraction tests')
32
class ExtractorTests(SimpleTestCase):
33

34
    test_dir = os.path.abspath(os.path.join(this_directory, 'commands'))
35

Tim Graham's avatar
Tim Graham committed
36
    PO_FILE = 'locale/%s/LC_MESSAGES/django.po' % LOCALE
37 38 39 40 41 42 43 44 45

    def setUp(self):
        self._cwd = os.getcwd()

    def _rmrf(self, dname):
        if os.path.commonprefix([self.test_dir, os.path.abspath(dname)]) != self.test_dir:
            return
        shutil.rmtree(dname)

46 47 48 49
    def rmfile(self, filepath):
        if os.path.exists(filepath):
            os.remove(filepath)

50 51 52 53 54 55 56 57
    def tearDown(self):
        os.chdir(self.test_dir)
        try:
            self._rmrf('locale/%s' % LOCALE)
        except OSError:
            pass
        os.chdir(self._cwd)

58
    def assertMsgId(self, msgid, s, use_quotes=True):
59
        q = '"'
60 61
        if use_quotes:
            msgid = '"%s"' % msgid
62 63
            q = "'"
        needle = 'msgid %s' % msgid
64
        msgid = re.escape(msgid)
Alex Gaynor's avatar
Alex Gaynor committed
65
        return self.assertTrue(re.search('^msgid %s' % msgid, s, re.MULTILINE), 'Could not find %(q)s%(n)s%(q)s in generated PO file' % {'n': needle, 'q': q})
66

67 68 69
    def assertNotMsgId(self, msgid, s, use_quotes=True):
        if use_quotes:
            msgid = '"%s"' % msgid
70
        msgid = re.escape(msgid)
71
        return self.assertTrue(not re.search('^msgid %s' % msgid, s, re.MULTILINE))
72

73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106
    def _assertPoLocComment(self, assert_presence, po_filename, line_number, *comment_parts):
        with open(po_filename, 'r') as fp:
            po_contents = force_text(fp.read())
        if os.name == 'nt':
            # #: .\path\to\file.html:123
            cwd_prefix = '%s%s' % (os.curdir, os.sep)
        else:
            # #: path/to/file.html:123
            cwd_prefix = ''
        parts = ['#: ']
        parts.append(os.path.join(cwd_prefix, *comment_parts))
        if line_number is not None:
            parts.append(':%d' % line_number)
        needle = ''.join(parts)
        if assert_presence:
            return self.assertTrue(needle in po_contents, '"%s" not found in final .po file.' % needle)
        else:
            return self.assertFalse(needle in po_contents, '"%s" shouldn\'t be in final .po file.' % needle)

    def assertLocationCommentPresent(self, po_filename, line_number, *comment_parts):
        """
        self.assertLocationCommentPresent('django.po', 42, 'dirA', 'dirB', 'foo.py')

        verifies that the django.po file has a gettext-style location comment of the form

        `#: dirA/dirB/foo.py:42`

        (or `#: .\dirA\dirB\foo.py:42` on Windows)

        None can be passed for the line_number argument to skip checking of the :42 suffix part.
        """
        return self._assertPoLocComment(True, po_filename, line_number, *comment_parts)

    def assertLocationCommentNotPresent(self, po_filename, line_number, *comment_parts):
107
        """Check the opposite of assertLocationComment()"""
108 109
        return self._assertPoLocComment(False, po_filename, line_number, *comment_parts)

110 111 112 113 114 115 116 117 118 119 120 121 122 123
    def assertRecentlyModified(self, path):
        """
        Assert that file was recently modified (modification time was less than 10 seconds ago).
        """
        delta = time.time() - os.stat(path).st_mtime
        self.assertLess(delta, 10, "%s was recently modified" % path)

    def assertNotRecentlyModified(self, path):
        """
        Assert that file was not recently modified (modification time was more than 10 seconds ago).
        """
        delta = time.time() - os.stat(path).st_mtime
        self.assertGreater(delta, 10, "%s wasn't recently modified" % path)

124

125 126 127 128
class BasicExtractorTests(ExtractorTests):

    def test_comments_extractor(self):
        os.chdir(self.test_dir)
129
        management.call_command('makemessages', locale=[LOCALE], verbosity=0)
130
        self.assertTrue(os.path.exists(self.PO_FILE))
131 132
        with io.open(self.PO_FILE, 'r', encoding='utf-8') as fp:
            po_contents = fp.read()
133 134 135 136 137
            self.assertTrue('#. Translators: This comment should be extracted' in po_contents)
            self.assertTrue('This comment should not be extracted' not in po_contents)
            # Comments in templates
            self.assertTrue('#. Translators: Django template comment for translators' in po_contents)
            self.assertTrue("#. Translators: Django comment block for translators\n#. string's meaning unveiled" in po_contents)
138

139 140
            self.assertTrue('#. Translators: One-line translator comment #1' in po_contents)
            self.assertTrue('#. Translators: Two-line translator comment #1\n#. continued here.' in po_contents)
141

142 143
            self.assertTrue('#. Translators: One-line translator comment #2' in po_contents)
            self.assertTrue('#. Translators: Two-line translator comment #2\n#. continued here.' in po_contents)
144

145 146
            self.assertTrue('#. Translators: One-line translator comment #3' in po_contents)
            self.assertTrue('#. Translators: Two-line translator comment #3\n#. continued here.' in po_contents)
147

148 149
            self.assertTrue('#. Translators: One-line translator comment #4' in po_contents)
            self.assertTrue('#. Translators: Two-line translator comment #4\n#. continued here.' in po_contents)
150

151 152
            self.assertTrue('#. Translators: One-line translator comment #5 -- with non ASCII characters: áéíóúö' in po_contents)
            self.assertTrue('#. Translators: Two-line translator comment #5 -- with non ASCII characters: áéíóúö\n#. continued here.' in po_contents)
153

154 155
    def test_templatize_trans_tag(self):
        # ticket #11240
156
        os.chdir(self.test_dir)
157
        management.call_command('makemessages', locale=[LOCALE], verbosity=0)
158
        self.assertTrue(os.path.exists(self.PO_FILE))
159
        with open(self.PO_FILE, 'r') as fp:
160
            po_contents = force_text(fp.read())
161 162 163 164 165 166 167 168 169 170 171 172
            self.assertMsgId('Literal with a percent symbol at the end %%', po_contents)
            self.assertMsgId('Literal with a percent %% symbol in the middle', po_contents)
            self.assertMsgId('Completed 50%% of all the tasks', po_contents)
            self.assertMsgId('Completed 99%% of all the tasks', po_contents)
            self.assertMsgId("Shouldn't double escape this sequence: %% (two percent signs)", po_contents)
            self.assertMsgId("Shouldn't double escape this sequence %% either", po_contents)
            self.assertMsgId("Looks like a str fmt spec %%s but shouldn't be interpreted as such", po_contents)
            self.assertMsgId("Looks like a str fmt spec %% o but shouldn't be interpreted as such", po_contents)

    def test_templatize_blocktrans_tag(self):
        # ticket #11966
        os.chdir(self.test_dir)
173
        management.call_command('makemessages', locale=[LOCALE], verbosity=0)
174 175
        self.assertTrue(os.path.exists(self.PO_FILE))
        with open(self.PO_FILE, 'r') as fp:
176
            po_contents = force_text(fp.read())
177 178
            self.assertMsgId('I think that 100%% is more that 50%% of anything.', po_contents)
            self.assertMsgId('I think that 100%% is more that 50%% of %(obj)s.', po_contents)
179
            self.assertMsgId("Blocktrans extraction shouldn't double escape this: %%, a=%(a)s", po_contents)
180

181 182
    def test_blocktrans_trimmed(self):
        os.chdir(self.test_dir)
183
        management.call_command('makemessages', locale=[LOCALE], verbosity=0)
184 185 186 187 188 189 190
        self.assertTrue(os.path.exists(self.PO_FILE))
        with open(self.PO_FILE, 'r') as fp:
            po_contents = force_text(fp.read())
            # should not be trimmed
            self.assertNotMsgId('Text with a few line breaks.', po_contents)
            # should be trimmed
            self.assertMsgId("Again some text with a few line breaks, this time should be trimmed.", po_contents)
191 192 193
        # #21406 -- Should adjust for eaten line numbers
        self.assertMsgId("I'm on line 97", po_contents)
        self.assertLocationCommentPresent(self.PO_FILE, 97, 'templates', 'test.html')
194

195 196 197 198 199
    def test_force_en_us_locale(self):
        """Value of locale-munging option used by the command is the right one"""
        from django.core.management.commands.makemessages import Command
        self.assertTrue(Command.leave_locale_alone)

200 201
    def test_extraction_error(self):
        os.chdir(self.test_dir)
202
        self.assertRaises(SyntaxError, management.call_command, 'makemessages', locale=[LOCALE], extensions=['tpl'], verbosity=0)
203
        with self.assertRaises(SyntaxError) as context_manager:
204
            management.call_command('makemessages', locale=[LOCALE], extensions=['tpl'], verbosity=0)
Jason Myers's avatar
Jason Myers committed
205 206 207 208
        six.assertRegex(
            self, str(context_manager.exception),
            r'Translation blocks must not include other block tags: blocktrans \(file templates[/\\]template_with_error\.tpl, line 3\)'
        )
209
        # Check that the temporary file was cleaned up
210
        self.assertFalse(os.path.exists('./templates/template_with_error.tpl.py'))
211

212 213 214 215 216
    def test_unicode_decode_error(self):
        os.chdir(self.test_dir)
        shutil.copyfile('./not_utf8.sample', './not_utf8.txt')
        self.addCleanup(self.rmfile, os.path.join(self.test_dir, 'not_utf8.txt'))
        stdout = StringIO()
217
        management.call_command('makemessages', locale=[LOCALE], stdout=stdout)
218 219 220
        self.assertIn("UnicodeDecodeError: skipped file not_utf8.txt in .",
                      force_text(stdout.getvalue()))

221
    def test_extraction_warning(self):
222
        """test xgettext warning about multiple bare interpolation placeholders"""
223 224
        os.chdir(self.test_dir)
        shutil.copyfile('./code.sample', './code_sample.py')
225
        self.addCleanup(self.rmfile, os.path.join(self.test_dir, 'code_sample.py'))
226
        stdout = StringIO()
227
        management.call_command('makemessages', locale=[LOCALE], stdout=stdout)
228
        self.assertIn("code_sample.py:4", force_text(stdout.getvalue()))
229

230 231 232 233 234 235 236
    def test_template_message_context_extractor(self):
        """
        Ensure that message contexts are correctly extracted for the
        {% trans %} and {% blocktrans %} template tags.
        Refs #14806.
        """
        os.chdir(self.test_dir)
237
        management.call_command('makemessages', locale=[LOCALE], verbosity=0)
238
        self.assertTrue(os.path.exists(self.PO_FILE))
239
        with open(self.PO_FILE, 'r') as fp:
240
            po_contents = force_text(fp.read())
241 242
            # {% trans %}
            self.assertTrue('msgctxt "Special trans context #1"' in po_contents)
243
            self.assertMsgId("Translatable literal #7a", po_contents)
244
            self.assertTrue('msgctxt "Special trans context #2"' in po_contents)
245
            self.assertMsgId("Translatable literal #7b", po_contents)
246
            self.assertTrue('msgctxt "Special trans context #3"' in po_contents)
247
            self.assertMsgId("Translatable literal #7c", po_contents)
248 249 250

            # {% blocktrans %}
            self.assertTrue('msgctxt "Special blocktrans context #1"' in po_contents)
251
            self.assertMsgId("Translatable literal #8a", po_contents)
252
            self.assertTrue('msgctxt "Special blocktrans context #2"' in po_contents)
253
            self.assertMsgId("Translatable literal #8b-singular", po_contents)
254 255
            self.assertTrue("Translatable literal #8b-plural" in po_contents)
            self.assertTrue('msgctxt "Special blocktrans context #3"' in po_contents)
256
            self.assertMsgId("Translatable literal #8c-singular", po_contents)
257 258
            self.assertTrue("Translatable literal #8c-plural" in po_contents)
            self.assertTrue('msgctxt "Special blocktrans context #4"' in po_contents)
259
            self.assertMsgId("Translatable literal #8d %(a)s", po_contents)
260

261 262
    def test_context_in_single_quotes(self):
        os.chdir(self.test_dir)
263
        management.call_command('makemessages', locale=[LOCALE], verbosity=0)
264 265
        self.assertTrue(os.path.exists(self.PO_FILE))
        with open(self.PO_FILE, 'r') as fp:
266
            po_contents = force_text(fp.read())
267 268 269 270 271 272 273 274
            # {% trans %}
            self.assertTrue('msgctxt "Context wrapped in double quotes"' in po_contents)
            self.assertTrue('msgctxt "Context wrapped in single quotes"' in po_contents)

            # {% blocktrans %}
            self.assertTrue('msgctxt "Special blocktrans context wrapped in double quotes"' in po_contents)
            self.assertTrue('msgctxt "Special blocktrans context wrapped in single quotes"' in po_contents)

275 276 277 278 279 280 281
    def test_template_comments(self):
        """Template comment tags on the same line of other constructs (#19552)"""
        os.chdir(self.test_dir)
        # Test detection/end user reporting of old, incorrect templates
        # translator comments syntax
        with warnings.catch_warnings(record=True) as ws:
            warnings.simplefilter('always')
282
            management.call_command('makemessages', locale=[LOCALE], extensions=['thtml'], verbosity=0)
283 284 285
            self.assertEqual(len(ws), 3)
            for w in ws:
                self.assertTrue(issubclass(w.category, TranslatorCommentWarning))
Jason Myers's avatar
Jason Myers committed
286 287
            six.assertRegex(
                self, str(ws[0].message),
288
                r"The translator-targeted comment 'Translators: ignored i18n comment #1' \(file templates[/\\]comments.thtml, line 4\) was ignored, because it wasn't the last item on the line\."
289
            )
Jason Myers's avatar
Jason Myers committed
290 291
            six.assertRegex(
                self, str(ws[1].message),
292
                r"The translator-targeted comment 'Translators: ignored i18n comment #3' \(file templates[/\\]comments.thtml, line 6\) was ignored, because it wasn't the last item on the line\."
293
            )
Jason Myers's avatar
Jason Myers committed
294 295
            six.assertRegex(
                self, str(ws[2].message),
296
                r"The translator-targeted comment 'Translators: ignored i18n comment #4' \(file templates[/\\]comments.thtml, line 8\) was ignored, because it wasn't the last item on the line\."
297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334
            )
        # Now test .po file contents
        self.assertTrue(os.path.exists(self.PO_FILE))
        with open(self.PO_FILE, 'r') as fp:
            po_contents = force_text(fp.read())

            self.assertMsgId('Translatable literal #9a', po_contents)
            self.assertFalse('ignored comment #1' in po_contents)

            self.assertFalse('Translators: ignored i18n comment #1' in po_contents)
            self.assertMsgId("Translatable literal #9b", po_contents)

            self.assertFalse('ignored i18n comment #2' in po_contents)
            self.assertFalse('ignored comment #2' in po_contents)
            self.assertMsgId('Translatable literal #9c', po_contents)

            self.assertFalse('ignored comment #3' in po_contents)
            self.assertFalse('ignored i18n comment #3' in po_contents)
            self.assertMsgId('Translatable literal #9d', po_contents)

            self.assertFalse('ignored comment #4' in po_contents)
            self.assertMsgId('Translatable literal #9e', po_contents)
            self.assertFalse('ignored comment #5' in po_contents)

            self.assertFalse('ignored i18n comment #4' in po_contents)
            self.assertMsgId('Translatable literal #9f', po_contents)
            self.assertTrue('#. Translators: valid i18n comment #5' in po_contents)

            self.assertMsgId('Translatable literal #9g', po_contents)
            self.assertTrue('#. Translators: valid i18n comment #6' in po_contents)
            self.assertMsgId('Translatable literal #9h', po_contents)
            self.assertTrue('#. Translators: valid i18n comment #7' in po_contents)
            self.assertMsgId('Translatable literal #9i', po_contents)

            six.assertRegex(self, po_contents, r'#\..+Translators: valid i18n comment #8')
            six.assertRegex(self, po_contents, r'#\..+Translators: valid i18n comment #9')
            self.assertMsgId("Translatable literal #9j", po_contents)

335

336 337
class JavascriptExtractorTests(ExtractorTests):

Tim Graham's avatar
Tim Graham committed
338
    PO_FILE = 'locale/%s/LC_MESSAGES/djangojs.po' % LOCALE
339 340 341

    def test_javascript_literals(self):
        os.chdir(self.test_dir)
342
        management.call_command('makemessages', domain='djangojs', locale=[LOCALE], verbosity=0)
343
        self.assertTrue(os.path.exists(self.PO_FILE))
344 345 346 347 348 349 350 351 352 353 354 355 356 357 358
        with open(self.PO_FILE, 'r') as fp:
            po_contents = fp.read()
            self.assertMsgId('This literal should be included.', po_contents)
            self.assertMsgId('This one as well.', po_contents)
            self.assertMsgId(r'He said, \"hello\".', po_contents)
            self.assertMsgId("okkkk", po_contents)
            self.assertMsgId("TEXT", po_contents)
            self.assertMsgId("It's at http://example.com", po_contents)
            self.assertMsgId("String", po_contents)
            self.assertMsgId("/* but this one will be too */ 'cause there is no way of telling...", po_contents)
            self.assertMsgId("foo", po_contents)
            self.assertMsgId("bar", po_contents)
            self.assertMsgId("baz", po_contents)
            self.assertMsgId("quz", po_contents)
            self.assertMsgId("foobar", po_contents)
359

Jason Myers's avatar
Jason Myers committed
360

361 362
class IgnoredExtractorTests(ExtractorTests):

363
    def _run_makemessages(self, **options):
364
        os.chdir(self.test_dir)
365
        stdout = StringIO()
366
        management.call_command('makemessages', locale=[LOCALE], verbosity=2,
367
            stdout=stdout, **options)
368
        data = stdout.getvalue()
369
        self.assertTrue(os.path.exists(self.PO_FILE))
370 371
        with open(self.PO_FILE, 'r') as fp:
            po_contents = fp.read()
372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395
        return data, po_contents

    def test_ignore_directory(self):
        out, po_contents = self._run_makemessages(ignore_patterns=[
            os.path.join('ignore_dir', '*'),
        ])
        self.assertTrue("ignoring directory ignore_dir" in out)
        self.assertMsgId('This literal should be included.', po_contents)
        self.assertNotMsgId('This should be ignored.', po_contents)

    def test_ignore_subdirectory(self):
        out, po_contents = self._run_makemessages(ignore_patterns=[
            'templates/*/ignore.html',
            'templates/subdir/*',
        ])
        self.assertTrue("ignoring directory subdir" in out)
        self.assertNotMsgId('This subdir should be ignored too.', po_contents)

    def test_ignore_file_patterns(self):
        out, po_contents = self._run_makemessages(ignore_patterns=[
            'xxx_*',
        ])
        self.assertTrue("ignoring file xxx_ignored.html" in out)
        self.assertNotMsgId('This should be ignored too.', po_contents)
396

397 398 399 400
    @override_settings(
        STATIC_ROOT=os.path.join(this_directory, 'commands', 'static_root/'),
        MEDIA_ROOT=os.path.join(this_directory, 'commands', 'media_root/'))
    def test_media_static_dirs_ignored(self):
401 402 403
        out, _ = self._run_makemessages()
        self.assertIn("ignoring directory static_root", out)
        self.assertIn("ignoring directory media_root", out)
404

405 406 407 408

class SymlinkExtractorTests(ExtractorTests):

    def setUp(self):
409
        super(SymlinkExtractorTests, self).setUp()
410 411 412 413 414 415 416 417 418 419 420 421
        self.symlinked_dir = os.path.join(self.test_dir, 'templates_symlinked')

    def tearDown(self):
        super(SymlinkExtractorTests, self).tearDown()
        os.chdir(self.test_dir)
        try:
            os.remove(self.symlinked_dir)
        except OSError:
            pass
        os.chdir(self._cwd)

    def test_symlink(self):
422
        # On Python < 3.2 os.symlink() exists only on Unix
423 424
        if hasattr(os, 'symlink'):
            if os.path.exists(self.symlinked_dir):
425
                self.assertTrue(os.path.islink(self.symlinked_dir))
426
            else:
427 428 429 430 431 432 433 434 435
                # On Python >= 3.2) os.symlink() exists always but then can
                # fail at runtime when user hasn't the needed permissions on
                # WIndows versions that support symbolink links (>= 6/Vista).
                # See Python issue 9333 (http://bugs.python.org/issue9333).
                # Skip the test in that case
                try:
                    os.symlink(os.path.join(self.test_dir, 'templates'), self.symlinked_dir)
                except (OSError, NotImplementedError):
                    raise SkipTest("os.symlink() is available on this OS but can't be used by this user.")
436
            os.chdir(self.test_dir)
437
            management.call_command('makemessages', locale=[LOCALE], verbosity=0, symlinks=True)
438
            self.assertTrue(os.path.exists(self.PO_FILE))
439
            with open(self.PO_FILE, 'r') as fp:
440
                po_contents = force_text(fp.read())
441 442
                self.assertMsgId('This literal should be included.', po_contents)
                self.assertTrue('templates_symlinked/test.html' in po_contents)
443 444 445


class CopyPluralFormsExtractorTests(ExtractorTests):
446

447 448 449
    PO_FILE_ES = 'locale/es/LC_MESSAGES/django.po'

    def tearDown(self):
450
        super(CopyPluralFormsExtractorTests, self).tearDown()
451 452 453 454 455 456
        os.chdir(self.test_dir)
        try:
            self._rmrf('locale/es')
        except OSError:
            pass
        os.chdir(self._cwd)
457 458 459

    def test_copy_plural_forms(self):
        os.chdir(self.test_dir)
460
        management.call_command('makemessages', locale=[LOCALE], verbosity=0)
461
        self.assertTrue(os.path.exists(self.PO_FILE))
462
        with open(self.PO_FILE, 'r') as fp:
463
            po_contents = force_text(fp.read())
464
            self.assertTrue('Plural-Forms: nplurals=2; plural=(n != 1)' in po_contents)
465

466 467 468
    def test_override_plural_forms(self):
        """Ticket #20311."""
        os.chdir(self.test_dir)
469
        management.call_command('makemessages', locale=['es'], extensions=['djtpl'], verbosity=0)
470
        self.assertTrue(os.path.exists(self.PO_FILE_ES))
471 472
        with io.open(self.PO_FILE_ES, 'r', encoding='utf-8') as fp:
            po_contents = fp.read()
473 474 475
            found = re.findall(r'^(?P<value>"Plural-Forms.+?\\n")\s*$', po_contents, re.MULTILINE | re.DOTALL)
            self.assertEqual(1, len(found))

476 477 478 479 480

class NoWrapExtractorTests(ExtractorTests):

    def test_no_wrap_enabled(self):
        os.chdir(self.test_dir)
481
        management.call_command('makemessages', locale=[LOCALE], verbosity=0, no_wrap=True)
482
        self.assertTrue(os.path.exists(self.PO_FILE))
483
        with open(self.PO_FILE, 'r') as fp:
484
            po_contents = force_text(fp.read())
485
            self.assertMsgId('This literal should also be included wrapped or not wrapped depending on the use of the --no-wrap option.', po_contents)
486 487 488

    def test_no_wrap_disabled(self):
        os.chdir(self.test_dir)
489
        management.call_command('makemessages', locale=[LOCALE], verbosity=0, no_wrap=False)
490
        self.assertTrue(os.path.exists(self.PO_FILE))
491
        with open(self.PO_FILE, 'r') as fp:
492
            po_contents = force_text(fp.read())
493
            self.assertMsgId('""\n"This literal should also be included wrapped or not wrapped depending on the "\n"use of the --no-wrap option."', po_contents, use_quotes=False)
494 495


496
class LocationCommentsTests(ExtractorTests):
497 498

    def test_no_location_enabled(self):
499
        """Behavior is correct if --no-location switch is specified. See #16903."""
500
        os.chdir(self.test_dir)
501
        management.call_command('makemessages', locale=[LOCALE], verbosity=0, no_location=True)
502
        self.assertTrue(os.path.exists(self.PO_FILE))
503
        self.assertLocationCommentNotPresent(self.PO_FILE, 55, 'templates', 'test.html.py')
504 505

    def test_no_location_disabled(self):
506
        """Behavior is correct if --no-location switch isn't specified."""
507
        os.chdir(self.test_dir)
508
        management.call_command('makemessages', locale=[LOCALE], verbosity=0, no_location=False)
509
        self.assertTrue(os.path.exists(self.PO_FILE))
510 511 512 513 514
        # #16903 -- Standard comment with source file relative path should be present
        self.assertLocationCommentPresent(self.PO_FILE, 55, 'templates', 'test.html')

        # #21208 -- Leaky paths in comments on Windows e.g. #: path\to\file.html.py:123
        self.assertLocationCommentNotPresent(self.PO_FILE, None, 'templates', 'test.html.py')
515 516 517 518


class KeepPotFileExtractorTests(ExtractorTests):

Tim Graham's avatar
Tim Graham committed
519
    POT_FILE = 'locale/django.pot'
520

521 522 523 524 525 526 527 528 529 530 531 532 533 534
    def setUp(self):
        super(KeepPotFileExtractorTests, self).setUp()

    def tearDown(self):
        super(KeepPotFileExtractorTests, self).tearDown()
        os.chdir(self.test_dir)
        try:
            os.unlink(self.POT_FILE)
        except OSError:
            pass
        os.chdir(self._cwd)

    def test_keep_pot_disabled_by_default(self):
        os.chdir(self.test_dir)
535
        management.call_command('makemessages', locale=[LOCALE], verbosity=0)
536 537 538 539
        self.assertFalse(os.path.exists(self.POT_FILE))

    def test_keep_pot_explicitly_disabled(self):
        os.chdir(self.test_dir)
540
        management.call_command('makemessages', locale=[LOCALE], verbosity=0,
541 542 543 544 545
                                keep_pot=False)
        self.assertFalse(os.path.exists(self.POT_FILE))

    def test_keep_pot_enabled(self):
        os.chdir(self.test_dir)
546
        management.call_command('makemessages', locale=[LOCALE], verbosity=0,
547 548
                                keep_pot=True)
        self.assertTrue(os.path.exists(self.POT_FILE))
549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566


class MultipleLocaleExtractionTests(ExtractorTests):
    PO_FILE_PT = 'locale/pt/LC_MESSAGES/django.po'
    PO_FILE_DE = 'locale/de/LC_MESSAGES/django.po'
    LOCALES = ['pt', 'de', 'ch']

    def tearDown(self):
        os.chdir(self.test_dir)
        for locale in self.LOCALES:
            try:
                self._rmrf('locale/%s' % locale)
            except OSError:
                pass
        os.chdir(self._cwd)

    def test_multiple_locales(self):
        os.chdir(self.test_dir)
Alex Gaynor's avatar
Alex Gaynor committed
567
        management.call_command('makemessages', locale=['pt', 'de'], verbosity=0)
568 569
        self.assertTrue(os.path.exists(self.PO_FILE_PT))
        self.assertTrue(os.path.exists(self.PO_FILE_DE))
570 571


572 573 574 575 576
class ExcludedLocaleExtractionTests(ExtractorTests):

    LOCALES = ['en', 'fr', 'it']
    PO_FILE = 'locale/%s/LC_MESSAGES/django.po'

577
    test_dir = os.path.abspath(os.path.join(this_directory, 'exclude'))
578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593

    def _set_times_for_all_po_files(self):
        """
        Set access and modification times to the Unix epoch time for all the .po files.
        """
        for locale in self.LOCALES:
            os.utime(self.PO_FILE % locale, (0, 0))

    def setUp(self):
        super(ExcludedLocaleExtractionTests, self).setUp()

        os.chdir(self.test_dir)  # ExtractorTests.tearDown() takes care of restoring.
        shutil.copytree('canned_locale', 'locale')
        self._set_times_for_all_po_files()
        self.addCleanup(self._rmrf, os.path.join(self.test_dir, 'locale'))

594 595 596 597 598 599 600 601 602 603 604
    def test_command_help(self):
        old_stdout, old_stderr = sys.stdout, sys.stderr
        sys.stdout, sys.stderr = StringIO(), StringIO()
        try:
            # `call_command` bypasses the parser; by calling
            # `execute_from_command_line` with the help subcommand we
            # ensure that there are no issues with the parser itself.
            execute_from_command_line(['django-admin', 'help', 'makemessages'])
        finally:
            sys.stdout, sys.stderr = old_stdout, old_stderr

605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630
    def test_one_locale_excluded(self):
        management.call_command('makemessages', exclude=['it'], stdout=StringIO())
        self.assertRecentlyModified(self.PO_FILE % 'en')
        self.assertRecentlyModified(self.PO_FILE % 'fr')
        self.assertNotRecentlyModified(self.PO_FILE % 'it')

    def test_multiple_locales_excluded(self):
        management.call_command('makemessages', exclude=['it', 'fr'], stdout=StringIO())
        self.assertRecentlyModified(self.PO_FILE % 'en')
        self.assertNotRecentlyModified(self.PO_FILE % 'fr')
        self.assertNotRecentlyModified(self.PO_FILE % 'it')

    def test_one_locale_excluded_with_locale(self):
        management.call_command('makemessages', locale=['en', 'fr'], exclude=['fr'], stdout=StringIO())
        self.assertRecentlyModified(self.PO_FILE % 'en')
        self.assertNotRecentlyModified(self.PO_FILE % 'fr')
        self.assertNotRecentlyModified(self.PO_FILE % 'it')

    def test_multiple_locales_excluded_with_locale(self):
        management.call_command('makemessages', locale=['en', 'fr', 'it'], exclude=['fr', 'it'],
                                stdout=StringIO())
        self.assertRecentlyModified(self.PO_FILE % 'en')
        self.assertNotRecentlyModified(self.PO_FILE % 'fr')
        self.assertNotRecentlyModified(self.PO_FILE % 'it')


631
class CustomLayoutExtractionTests(ExtractorTests):
632

633 634
    def setUp(self):
        self._cwd = os.getcwd()
635
        self.test_dir = os.path.join(this_directory, 'project_dir')
636 637 638 639 640 641 642 643 644

    def test_no_locale_raises(self):
        os.chdir(self.test_dir)
        with six.assertRaisesRegex(self, management.CommandError,
                "Unable to find a locale path to store translations for file"):
            management.call_command('makemessages', locale=LOCALE, verbosity=0)

    @override_settings(
        LOCALE_PATHS=(os.path.join(
645
            this_directory, 'project_dir', 'project_locale'),)
646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673
    )
    def test_project_locale_paths(self):
        """
        Test that:
          * translations for an app containing a locale folder are stored in that folder
          * translations outside of that app are in LOCALE_PATHS[0]
        """
        os.chdir(self.test_dir)
        self.addCleanup(shutil.rmtree,
            os.path.join(settings.LOCALE_PATHS[0], LOCALE), True)
        self.addCleanup(shutil.rmtree,
            os.path.join(self.test_dir, 'app_with_locale', 'locale', LOCALE), True)

        management.call_command('makemessages', locale=[LOCALE], verbosity=0)
        project_de_locale = os.path.join(
            self.test_dir, 'project_locale', 'de', 'LC_MESSAGES', 'django.po')
        app_de_locale = os.path.join(
            self.test_dir, 'app_with_locale', 'locale', 'de', 'LC_MESSAGES', 'django.po')
        self.assertTrue(os.path.exists(project_de_locale))
        self.assertTrue(os.path.exists(app_de_locale))

        with open(project_de_locale, 'r') as fp:
            po_contents = force_text(fp.read())
            self.assertMsgId('This app has no locale directory', po_contents)
            self.assertMsgId('This is a project-level string', po_contents)
        with open(app_de_locale, 'r') as fp:
            po_contents = force_text(fp.read())
            self.assertMsgId('This app has a locale directory', po_contents)