project_dir = os.path.join(data_dir, 'project')
i18n_dir = os.path.join(project_dir, 'i18n')
pot_file = os.path.join(i18n_dir, 'temp.pot')
+
+
+def get_po_file_path(locale):
+ return os.path.join(i18n_dir, locale, 'LC_MESSAGES', 'messages.po')
--- /dev/null
+#
+# Copyright (C) 2007-2011 Edgewall Software, 2013-2025 the Babel team
+# All rights reserved.
+#
+# This software is licensed as described in the file LICENSE, which
+# you should have received as part of this distribution. The terms
+# are also available at https://github.com/python-babel/babel/blob/master/LICENSE.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at https://github.com/python-babel/babel/commits/master/.
+
+from __future__ import annotations
+
+import logging
+import os
+import shutil
+import sys
+import time
+import unittest
+from datetime import datetime, timedelta
+from io import StringIO
+
+import pytest
+from freezegun import freeze_time
+
+from babel import __version__ as VERSION
+from babel.dates import format_datetime
+from babel.messages import Catalog, frontend
+from babel.messages.frontend import BaseError
+from babel.messages.pofile import read_po, write_po
+from babel.util import LOCALTZ
+from tests.messages.consts import data_dir, get_po_file_path, i18n_dir, pot_file, this_dir
+
+
+class CommandLineInterfaceTestCase(unittest.TestCase):
+
+ def setUp(self):
+ data_dir = os.path.join(this_dir, 'data')
+ self.orig_working_dir = os.getcwd()
+ self.orig_argv = sys.argv
+ self.orig_stdout = sys.stdout
+ self.orig_stderr = sys.stderr
+ sys.argv = ['pybabel']
+ sys.stdout = StringIO()
+ sys.stderr = StringIO()
+ os.chdir(data_dir)
+
+ self._remove_log_handlers()
+ self.cli = frontend.CommandLineInterface()
+
+ def tearDown(self):
+ os.chdir(self.orig_working_dir)
+ sys.argv = self.orig_argv
+ sys.stdout = self.orig_stdout
+ sys.stderr = self.orig_stderr
+ for dirname in ['lv_LV', 'ja_JP']:
+ locale_dir = os.path.join(i18n_dir, dirname)
+ if os.path.isdir(locale_dir):
+ shutil.rmtree(locale_dir)
+ self._remove_log_handlers()
+
+ def _remove_log_handlers(self):
+ # Logging handlers will be reused if possible (#227). This breaks the
+ # implicit assumption that our newly created StringIO for sys.stderr
+ # contains the console output. Removing the old handler ensures that a
+ # new handler with our new StringIO instance will be used.
+ log = logging.getLogger('babel')
+ for handler in log.handlers:
+ log.removeHandler(handler)
+
+ def test_usage(self):
+ try:
+ self.cli.run(sys.argv)
+ self.fail('Expected SystemExit')
+ except SystemExit as e:
+ assert e.code == 2
+ assert sys.stderr.getvalue().lower() == """\
+usage: pybabel command [options] [args]
+
+pybabel: error: no valid command or option passed. try the -h/--help option for more information.
+"""
+
+ def test_list_locales(self):
+ """
+ Test the command with the --list-locales arg.
+ """
+ result = self.cli.run(sys.argv + ['--list-locales'])
+ assert not result
+ output = sys.stdout.getvalue()
+ assert 'fr_CH' in output
+ assert 'French (Switzerland)' in output
+ assert "\nb'" not in output # No bytes repr markers in output
+
+ def _run_init_catalog(self):
+ i18n_dir = os.path.join(data_dir, 'project', 'i18n')
+ pot_path = os.path.join(data_dir, 'project', 'i18n', 'messages.pot')
+ init_argv = sys.argv + ['init', '--locale', 'en_US', '-d', i18n_dir,
+ '-i', pot_path]
+ self.cli.run(init_argv)
+
+ def test_no_duplicated_output_for_multiple_runs(self):
+ self._run_init_catalog()
+ first_output = sys.stderr.getvalue()
+ self._run_init_catalog()
+ second_output = sys.stderr.getvalue()[len(first_output):]
+
+ # in case the log message is not duplicated we should get the same
+ # output as before
+ assert first_output == second_output
+
+ def test_frontend_can_log_to_predefined_handler(self):
+ custom_stream = StringIO()
+ log = logging.getLogger('babel')
+ log.addHandler(logging.StreamHandler(custom_stream))
+
+ self._run_init_catalog()
+ assert id(sys.stderr) != id(custom_stream)
+ assert not sys.stderr.getvalue()
+ assert custom_stream.getvalue()
+
+ def test_help(self):
+ try:
+ self.cli.run(sys.argv + ['--help'])
+ self.fail('Expected SystemExit')
+ except SystemExit as e:
+ assert not e.code
+ content = sys.stdout.getvalue().lower()
+ assert 'options:' in content
+ assert all(command in content for command in ('init', 'update', 'compile', 'extract'))
+
+ def assert_pot_file_exists(self):
+ assert os.path.isfile(pot_file)
+
+ @freeze_time("1994-11-11")
+ def test_extract_with_default_mapping(self):
+ self.cli.run(sys.argv + ['extract',
+ '--copyright-holder', 'FooBar, Inc.',
+ '--project', 'TestProject', '--version', '0.1',
+ '--msgid-bugs-address', 'bugs.address@email.tld',
+ '-c', 'TRANSLATOR', '-c', 'TRANSLATORS:',
+ '-o', pot_file, 'project'])
+ self.assert_pot_file_exists()
+ date = format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en')
+ expected_content = fr"""# Translations template for TestProject.
+# Copyright (C) {time.strftime('%Y')} FooBar, Inc.
+# This file is distributed under the same license as the TestProject
+# project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, {time.strftime('%Y')}.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: TestProject 0.1\n"
+"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
+"POT-Creation-Date: {date}\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel {VERSION}\n"
+
+#. TRANSLATOR: This will be a translator coment,
+#. that will include several lines
+#: project/file1.py:8
+msgid "bar"
+msgstr ""
+
+#: project/file2.py:9
+msgid "foobar"
+msgid_plural "foobars"
+msgstr[0] ""
+msgstr[1] ""
+
+#: project/ignored/this_wont_normally_be_here.py:11
+msgid "FooBar"
+msgid_plural "FooBars"
+msgstr[0] ""
+msgstr[1] ""
+
+"""
+ with open(pot_file) as f:
+ actual_content = f.read()
+ assert expected_content == actual_content
+
+ @freeze_time("1994-11-11")
+ def test_extract_with_mapping_file(self):
+ self.cli.run(sys.argv + ['extract',
+ '--copyright-holder', 'FooBar, Inc.',
+ '--project', 'TestProject', '--version', '0.1',
+ '--msgid-bugs-address', 'bugs.address@email.tld',
+ '--mapping', os.path.join(data_dir, 'mapping.cfg'),
+ '-c', 'TRANSLATOR', '-c', 'TRANSLATORS:',
+ '-o', pot_file, 'project'])
+ self.assert_pot_file_exists()
+ date = format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en')
+ expected_content = fr"""# Translations template for TestProject.
+# Copyright (C) {time.strftime('%Y')} FooBar, Inc.
+# This file is distributed under the same license as the TestProject
+# project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, {time.strftime('%Y')}.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: TestProject 0.1\n"
+"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
+"POT-Creation-Date: {date}\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel {VERSION}\n"
+
+#. TRANSLATOR: This will be a translator coment,
+#. that will include several lines
+#: project/file1.py:8
+msgid "bar"
+msgstr ""
+
+#: project/file2.py:9
+msgid "foobar"
+msgid_plural "foobars"
+msgstr[0] ""
+msgstr[1] ""
+
+"""
+ with open(pot_file) as f:
+ actual_content = f.read()
+ assert expected_content == actual_content
+
+ @freeze_time("1994-11-11")
+ def test_extract_with_exact_file(self):
+ """Tests that we can call extract with a particular file and only
+ strings from that file get extracted. (Note the absence of strings from file1.py)
+ """
+ file_to_extract = os.path.join(data_dir, 'project', 'file2.py')
+ self.cli.run(sys.argv + ['extract',
+ '--copyright-holder', 'FooBar, Inc.',
+ '--project', 'TestProject', '--version', '0.1',
+ '--msgid-bugs-address', 'bugs.address@email.tld',
+ '--mapping', os.path.join(data_dir, 'mapping.cfg'),
+ '-c', 'TRANSLATOR', '-c', 'TRANSLATORS:',
+ '-o', pot_file, file_to_extract])
+ self.assert_pot_file_exists()
+ date = format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en')
+ expected_content = fr"""# Translations template for TestProject.
+# Copyright (C) {time.strftime('%Y')} FooBar, Inc.
+# This file is distributed under the same license as the TestProject
+# project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, {time.strftime('%Y')}.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: TestProject 0.1\n"
+"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
+"POT-Creation-Date: {date}\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel {VERSION}\n"
+
+#: project/file2.py:9
+msgid "foobar"
+msgid_plural "foobars"
+msgstr[0] ""
+msgstr[1] ""
+
+"""
+ with open(pot_file) as f:
+ actual_content = f.read()
+ assert expected_content == actual_content
+
+ @freeze_time("1994-11-11")
+ def test_init_with_output_dir(self):
+ po_file = get_po_file_path('en_US')
+ self.cli.run(sys.argv + ['init',
+ '--locale', 'en_US',
+ '-d', os.path.join(i18n_dir),
+ '-i', os.path.join(i18n_dir, 'messages.pot')])
+ assert os.path.isfile(po_file)
+ date = format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en')
+ expected_content = fr"""# English (United States) translations for TestProject.
+# Copyright (C) 2007 FooBar, Inc.
+# This file is distributed under the same license as the TestProject
+# project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: TestProject 0.1\n"
+"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
+"POT-Creation-Date: 2007-04-01 15:30+0200\n"
+"PO-Revision-Date: {date}\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language: en_US\n"
+"Language-Team: en_US <LL@li.org>\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel {VERSION}\n"
+
+#. This will be a translator coment,
+#. that will include several lines
+#: project/file1.py:8
+msgid "bar"
+msgstr ""
+
+#: project/file2.py:9
+msgid "foobar"
+msgid_plural "foobars"
+msgstr[0] ""
+msgstr[1] ""
+
+"""
+ with open(po_file) as f:
+ actual_content = f.read()
+ assert expected_content == actual_content
+
+ @freeze_time("1994-11-11")
+ def test_init_singular_plural_forms(self):
+ po_file = get_po_file_path('ja_JP')
+ self.cli.run(sys.argv + ['init',
+ '--locale', 'ja_JP',
+ '-d', os.path.join(i18n_dir),
+ '-i', os.path.join(i18n_dir, 'messages.pot')])
+ assert os.path.isfile(po_file)
+ date = format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en')
+ expected_content = fr"""# Japanese (Japan) translations for TestProject.
+# Copyright (C) 2007 FooBar, Inc.
+# This file is distributed under the same license as the TestProject
+# project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: TestProject 0.1\n"
+"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
+"POT-Creation-Date: 2007-04-01 15:30+0200\n"
+"PO-Revision-Date: {date}\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language: ja_JP\n"
+"Language-Team: ja_JP <LL@li.org>\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel {VERSION}\n"
+
+#. This will be a translator coment,
+#. that will include several lines
+#: project/file1.py:8
+msgid "bar"
+msgstr ""
+
+#: project/file2.py:9
+msgid "foobar"
+msgid_plural "foobars"
+msgstr[0] ""
+
+"""
+ with open(po_file) as f:
+ actual_content = f.read()
+ assert expected_content == actual_content
+
+ @freeze_time("1994-11-11")
+ def test_init_more_than_2_plural_forms(self):
+ po_file = get_po_file_path('lv_LV')
+ self.cli.run(sys.argv + ['init',
+ '--locale', 'lv_LV',
+ '-d', i18n_dir,
+ '-i', os.path.join(i18n_dir, 'messages.pot')])
+ assert os.path.isfile(po_file)
+ date = format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en')
+ expected_content = fr"""# Latvian (Latvia) translations for TestProject.
+# Copyright (C) 2007 FooBar, Inc.
+# This file is distributed under the same license as the TestProject
+# project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: TestProject 0.1\n"
+"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
+"POT-Creation-Date: 2007-04-01 15:30+0200\n"
+"PO-Revision-Date: {date}\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language: lv_LV\n"
+"Language-Team: lv_LV <LL@li.org>\n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 :"
+" 2);\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel {VERSION}\n"
+
+#. This will be a translator coment,
+#. that will include several lines
+#: project/file1.py:8
+msgid "bar"
+msgstr ""
+
+#: project/file2.py:9
+msgid "foobar"
+msgid_plural "foobars"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+
+"""
+ with open(po_file) as f:
+ actual_content = f.read()
+ assert expected_content == actual_content
+
+ def test_compile_catalog(self):
+ po_file = get_po_file_path('de_DE')
+ mo_file = po_file.replace('.po', '.mo')
+ self.cli.run(sys.argv + ['compile',
+ '--locale', 'de_DE',
+ '-d', i18n_dir])
+ assert not os.path.isfile(mo_file), f'Expected no file at {mo_file!r}'
+ assert sys.stderr.getvalue() == f'catalog {po_file} is marked as fuzzy, skipping\n'
+
+ def test_compile_fuzzy_catalog(self):
+ po_file = get_po_file_path('de_DE')
+ mo_file = po_file.replace('.po', '.mo')
+ try:
+ self.cli.run(sys.argv + ['compile',
+ '--locale', 'de_DE', '--use-fuzzy',
+ '-d', i18n_dir])
+ assert os.path.isfile(mo_file)
+ assert sys.stderr.getvalue() == f'compiling catalog {po_file} to {mo_file}\n'
+ finally:
+ if os.path.isfile(mo_file):
+ os.unlink(mo_file)
+
+ def test_compile_catalog_with_more_than_2_plural_forms(self):
+ po_file = get_po_file_path('ru_RU')
+ mo_file = po_file.replace('.po', '.mo')
+ try:
+ self.cli.run(sys.argv + ['compile',
+ '--locale', 'ru_RU', '--use-fuzzy',
+ '-d', i18n_dir])
+ assert os.path.isfile(mo_file)
+ assert sys.stderr.getvalue() == f'compiling catalog {po_file} to {mo_file}\n'
+ finally:
+ if os.path.isfile(mo_file):
+ os.unlink(mo_file)
+
+ def test_compile_catalog_multidomain(self):
+ po_foo = os.path.join(i18n_dir, 'de_DE', 'LC_MESSAGES', 'foo.po')
+ po_bar = os.path.join(i18n_dir, 'de_DE', 'LC_MESSAGES', 'bar.po')
+ mo_foo = po_foo.replace('.po', '.mo')
+ mo_bar = po_bar.replace('.po', '.mo')
+ try:
+ self.cli.run(sys.argv + ['compile',
+ '--locale', 'de_DE', '--domain', 'foo bar', '--use-fuzzy',
+ '-d', i18n_dir])
+ for mo_file in [mo_foo, mo_bar]:
+ assert os.path.isfile(mo_file)
+ assert sys.stderr.getvalue() == (
+ f'compiling catalog {po_foo} to {mo_foo}\n'
+ f'compiling catalog {po_bar} to {mo_bar}\n'
+ )
+
+ finally:
+ for mo_file in [mo_foo, mo_bar]:
+ if os.path.isfile(mo_file):
+ os.unlink(mo_file)
+
+ def test_update(self):
+ template = Catalog()
+ template.add("1")
+ template.add("2")
+ template.add("3")
+ tmpl_file = os.path.join(i18n_dir, 'temp-template.pot')
+ with open(tmpl_file, "wb") as outfp:
+ write_po(outfp, template)
+ po_file = os.path.join(i18n_dir, 'temp1.po')
+ self.cli.run(sys.argv + ['init',
+ '-l', 'fi',
+ '-o', po_file,
+ '-i', tmpl_file,
+ ])
+ with open(po_file) as infp:
+ catalog = read_po(infp)
+ assert len(catalog) == 3
+
+ # Add another entry to the template
+
+ template.add("4")
+
+ with open(tmpl_file, "wb") as outfp:
+ write_po(outfp, template)
+
+ self.cli.run(sys.argv + ['update',
+ '-l', 'fi_FI',
+ '-o', po_file,
+ '-i', tmpl_file])
+
+ with open(po_file) as infp:
+ catalog = read_po(infp)
+ assert len(catalog) == 4 # Catalog was updated
+
+ def test_update_pot_creation_date(self):
+ template = Catalog()
+ template.add("1")
+ template.add("2")
+ template.add("3")
+ tmpl_file = os.path.join(i18n_dir, 'temp-template.pot')
+ with open(tmpl_file, "wb") as outfp:
+ write_po(outfp, template)
+ po_file = os.path.join(i18n_dir, 'temp1.po')
+ self.cli.run(sys.argv + ['init',
+ '-l', 'fi',
+ '-o', po_file,
+ '-i', tmpl_file,
+ ])
+ with open(po_file) as infp:
+ catalog = read_po(infp)
+ assert len(catalog) == 3
+ original_catalog_creation_date = catalog.creation_date
+
+ # Update the template creation date
+ template.creation_date -= timedelta(minutes=3)
+ with open(tmpl_file, "wb") as outfp:
+ write_po(outfp, template)
+
+ self.cli.run(sys.argv + ['update',
+ '-l', 'fi_FI',
+ '-o', po_file,
+ '-i', tmpl_file])
+
+ with open(po_file) as infp:
+ catalog = read_po(infp)
+ # We didn't ignore the creation date, so expect a diff
+ assert catalog.creation_date != original_catalog_creation_date
+
+ # Reset the "original"
+ original_catalog_creation_date = catalog.creation_date
+
+ # Update the template creation date again
+ # This time, pass the ignore flag and expect the times are different
+ template.creation_date -= timedelta(minutes=5)
+ with open(tmpl_file, "wb") as outfp:
+ write_po(outfp, template)
+
+ self.cli.run(sys.argv + ['update',
+ '-l', 'fi_FI',
+ '-o', po_file,
+ '-i', tmpl_file,
+ '--ignore-pot-creation-date'])
+
+ with open(po_file) as infp:
+ catalog = read_po(infp)
+ # We ignored creation date, so it should not have changed
+ assert catalog.creation_date == original_catalog_creation_date
+
+ def test_check(self):
+ template = Catalog()
+ template.add("1")
+ template.add("2")
+ template.add("3")
+ tmpl_file = os.path.join(i18n_dir, 'temp-template.pot')
+ with open(tmpl_file, "wb") as outfp:
+ write_po(outfp, template)
+ po_file = os.path.join(i18n_dir, 'temp1.po')
+ self.cli.run(sys.argv + ['init',
+ '-l', 'fi_FI',
+ '-o', po_file,
+ '-i', tmpl_file,
+ ])
+
+ # Update the catalog file
+ self.cli.run(sys.argv + ['update',
+ '-l', 'fi_FI',
+ '-o', po_file,
+ '-i', tmpl_file])
+
+ # Run a check without introducing any changes to the template
+ self.cli.run(sys.argv + ['update',
+ '--check',
+ '-l', 'fi_FI',
+ '-o', po_file,
+ '-i', tmpl_file])
+
+ # Add a new entry and expect the check to fail
+ template.add("4")
+ with open(tmpl_file, "wb") as outfp:
+ write_po(outfp, template)
+
+ with pytest.raises(BaseError):
+ self.cli.run(sys.argv + ['update',
+ '--check',
+ '-l', 'fi_FI',
+ '-o', po_file,
+ '-i', tmpl_file])
+
+ # Write the latest changes to the po-file
+ self.cli.run(sys.argv + ['update',
+ '-l', 'fi_FI',
+ '-o', po_file,
+ '-i', tmpl_file])
+
+ # Update an entry and expect the check to fail
+ template.add("4", locations=[("foo.py", 1)])
+ with open(tmpl_file, "wb") as outfp:
+ write_po(outfp, template)
+
+ with pytest.raises(BaseError):
+ self.cli.run(sys.argv + ['update',
+ '--check',
+ '-l', 'fi_FI',
+ '-o', po_file,
+ '-i', tmpl_file])
+
+ def test_check_pot_creation_date(self):
+ template = Catalog()
+ template.add("1")
+ template.add("2")
+ template.add("3")
+ tmpl_file = os.path.join(i18n_dir, 'temp-template.pot')
+ with open(tmpl_file, "wb") as outfp:
+ write_po(outfp, template)
+ po_file = os.path.join(i18n_dir, 'temp1.po')
+ self.cli.run(sys.argv + ['init',
+ '-l', 'fi_FI',
+ '-o', po_file,
+ '-i', tmpl_file,
+ ])
+
+ # Update the catalog file
+ self.cli.run(sys.argv + ['update',
+ '-l', 'fi_FI',
+ '-o', po_file,
+ '-i', tmpl_file])
+
+ # Run a check without introducing any changes to the template
+ self.cli.run(sys.argv + ['update',
+ '--check',
+ '-l', 'fi_FI',
+ '-o', po_file,
+ '-i', tmpl_file])
+
+ # Run a check after changing the template creation date
+ template.creation_date = datetime.now() - timedelta(minutes=5)
+ with open(tmpl_file, "wb") as outfp:
+ write_po(outfp, template)
+
+ # Should fail without --ignore-pot-creation-date flag
+ with pytest.raises(BaseError):
+ self.cli.run(sys.argv + ['update',
+ '--check',
+ '-l', 'fi_FI',
+ '-o', po_file,
+ '-i', tmpl_file])
+ # Should pass with --ignore-pot-creation-date flag
+ self.cli.run(sys.argv + ['update',
+ '--check',
+ '-l', 'fi_FI',
+ '-o', po_file,
+ '-i', tmpl_file,
+ '--ignore-pot-creation-date'])
+
+ def test_update_init_missing(self):
+ template = Catalog()
+ template.add("1")
+ template.add("2")
+ template.add("3")
+ tmpl_file = os.path.join(i18n_dir, 'temp2-template.pot')
+ with open(tmpl_file, "wb") as outfp:
+ write_po(outfp, template)
+ po_file = os.path.join(i18n_dir, 'temp2.po')
+
+ self.cli.run(sys.argv + ['update',
+ '--init-missing',
+ '-l', 'fi',
+ '-o', po_file,
+ '-i', tmpl_file])
+
+ with open(po_file) as infp:
+ catalog = read_po(infp)
+ assert len(catalog) == 3
+
+ # Add another entry to the template
+
+ template.add("4")
+
+ with open(tmpl_file, "wb") as outfp:
+ write_po(outfp, template)
+
+ self.cli.run(sys.argv + ['update',
+ '--init-missing',
+ '-l', 'fi_FI',
+ '-o', po_file,
+ '-i', tmpl_file])
+
+ with open(po_file) as infp:
+ catalog = read_po(infp)
+ assert len(catalog) == 4 # Catalog was updated
--- /dev/null
+#
+# Copyright (C) 2007-2011 Edgewall Software, 2013-2025 the Babel team
+# All rights reserved.
+#
+# This software is licensed as described in the file LICENSE, which
+# you should have received as part of this distribution. The terms
+# are also available at https://github.com/python-babel/babel/blob/master/LICENSE.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at https://github.com/python-babel/babel/commits/master/.
+
+from __future__ import annotations
+
+import os
+import unittest
+
+import pytest
+
+from babel.messages import frontend
+from babel.messages.frontend import OptionError
+from tests.messages.consts import TEST_PROJECT_DISTRIBUTION_DATA, data_dir
+from tests.messages.utils import Distribution
+
+
+class CompileCatalogTestCase(unittest.TestCase):
+
+ def setUp(self):
+ self.olddir = os.getcwd()
+ os.chdir(data_dir)
+
+ self.dist = Distribution(TEST_PROJECT_DISTRIBUTION_DATA)
+ self.cmd = frontend.CompileCatalog(self.dist)
+ self.cmd.initialize_options()
+
+ def tearDown(self):
+ os.chdir(self.olddir)
+
+ def test_no_directory_or_output_file_specified(self):
+ self.cmd.locale = 'en_US'
+ self.cmd.input_file = 'dummy'
+ with pytest.raises(OptionError):
+ self.cmd.finalize_options()
+
+ def test_no_directory_or_input_file_specified(self):
+ self.cmd.locale = 'en_US'
+ self.cmd.output_file = 'dummy'
+ with pytest.raises(OptionError):
+ self.cmd.finalize_options()
--- /dev/null
+#
+# Copyright (C) 2007-2011 Edgewall Software, 2013-2025 the Babel team
+# All rights reserved.
+#
+# This software is licensed as described in the file LICENSE, which
+# you should have received as part of this distribution. The terms
+# are also available at https://github.com/python-babel/babel/blob/master/LICENSE.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at https://github.com/python-babel/babel/commits/master/.
+
+from __future__ import annotations
+
+import os
+import time
+import unittest
+from datetime import datetime
+
+import pytest
+from freezegun import freeze_time
+
+from babel import __version__ as VERSION
+from babel.dates import format_datetime
+from babel.messages import frontend
+from babel.messages.frontend import OptionError
+from babel.messages.pofile import read_po
+from babel.util import LOCALTZ
+from tests.messages.consts import TEST_PROJECT_DISTRIBUTION_DATA, data_dir, pot_file, this_dir
+from tests.messages.utils import Distribution
+
+
+class ExtractMessagesTestCase(unittest.TestCase):
+
+ def setUp(self):
+ self.olddir = os.getcwd()
+ os.chdir(data_dir)
+
+ self.dist = Distribution(TEST_PROJECT_DISTRIBUTION_DATA)
+ self.cmd = frontend.ExtractMessages(self.dist)
+ self.cmd.initialize_options()
+
+ def tearDown(self):
+ if os.path.isfile(pot_file):
+ os.unlink(pot_file)
+
+ os.chdir(self.olddir)
+
+ def assert_pot_file_exists(self):
+ assert os.path.isfile(pot_file)
+
+ def test_neither_default_nor_custom_keywords(self):
+ self.cmd.output_file = 'dummy'
+ self.cmd.no_default_keywords = True
+ with pytest.raises(OptionError):
+ self.cmd.finalize_options()
+
+ def test_no_output_file_specified(self):
+ with pytest.raises(OptionError):
+ self.cmd.finalize_options()
+
+ def test_both_sort_output_and_sort_by_file(self):
+ self.cmd.output_file = 'dummy'
+ self.cmd.sort_output = True
+ self.cmd.sort_by_file = True
+ with pytest.raises(OptionError):
+ self.cmd.finalize_options()
+
+ def test_invalid_file_or_dir_input_path(self):
+ self.cmd.input_paths = 'nonexistent_path'
+ self.cmd.output_file = 'dummy'
+ with pytest.raises(OptionError):
+ self.cmd.finalize_options()
+
+ def test_input_paths_is_treated_as_list(self):
+ self.cmd.input_paths = data_dir
+ self.cmd.output_file = pot_file
+ self.cmd.finalize_options()
+ self.cmd.run()
+
+ with open(pot_file) as f:
+ catalog = read_po(f)
+ msg = catalog.get('bar')
+ assert len(msg.locations) == 1
+ assert ('file1.py' in msg.locations[0][0])
+
+ def test_input_paths_handle_spaces_after_comma(self):
+ self.cmd.input_paths = f"{this_dir}, {data_dir}"
+ self.cmd.output_file = pot_file
+ self.cmd.finalize_options()
+ assert self.cmd.input_paths == [this_dir, data_dir]
+
+ def test_input_dirs_is_alias_for_input_paths(self):
+ self.cmd.input_dirs = this_dir
+ self.cmd.output_file = pot_file
+ self.cmd.finalize_options()
+ # Gets listified in `finalize_options`:
+ assert self.cmd.input_paths == [self.cmd.input_dirs]
+
+ def test_input_dirs_is_mutually_exclusive_with_input_paths(self):
+ self.cmd.input_dirs = this_dir
+ self.cmd.input_paths = this_dir
+ self.cmd.output_file = pot_file
+ with pytest.raises(OptionError):
+ self.cmd.finalize_options()
+
+ @freeze_time("1994-11-11")
+ def test_extraction_with_default_mapping(self):
+ self.cmd.copyright_holder = 'FooBar, Inc.'
+ self.cmd.msgid_bugs_address = 'bugs.address@email.tld'
+ self.cmd.output_file = 'project/i18n/temp.pot'
+ self.cmd.add_comments = 'TRANSLATOR:,TRANSLATORS:'
+
+ self.cmd.finalize_options()
+ self.cmd.run()
+
+ self.assert_pot_file_exists()
+
+ date = format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en')
+ expected_content = fr"""# Translations template for TestProject.
+# Copyright (C) {time.strftime('%Y')} FooBar, Inc.
+# This file is distributed under the same license as the TestProject
+# project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, {time.strftime('%Y')}.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: TestProject 0.1\n"
+"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
+"POT-Creation-Date: {date}\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel {VERSION}\n"
+
+#. TRANSLATOR: This will be a translator coment,
+#. that will include several lines
+#: project/file1.py:8
+msgid "bar"
+msgstr ""
+
+#: project/file2.py:9
+msgid "foobar"
+msgid_plural "foobars"
+msgstr[0] ""
+msgstr[1] ""
+
+#: project/ignored/this_wont_normally_be_here.py:11
+msgid "FooBar"
+msgid_plural "FooBars"
+msgstr[0] ""
+msgstr[1] ""
+
+"""
+ with open(pot_file) as f:
+ actual_content = f.read()
+ assert expected_content == actual_content
+
+ @freeze_time("1994-11-11")
+ def test_extraction_with_mapping_file(self):
+ self.cmd.copyright_holder = 'FooBar, Inc.'
+ self.cmd.msgid_bugs_address = 'bugs.address@email.tld'
+ self.cmd.mapping_file = 'mapping.cfg'
+ self.cmd.output_file = 'project/i18n/temp.pot'
+ self.cmd.add_comments = 'TRANSLATOR:,TRANSLATORS:'
+
+ self.cmd.finalize_options()
+ self.cmd.run()
+
+ self.assert_pot_file_exists()
+
+ date = format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en')
+ expected_content = fr"""# Translations template for TestProject.
+# Copyright (C) {time.strftime('%Y')} FooBar, Inc.
+# This file is distributed under the same license as the TestProject
+# project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, {time.strftime('%Y')}.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: TestProject 0.1\n"
+"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
+"POT-Creation-Date: {date}\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel {VERSION}\n"
+
+#. TRANSLATOR: This will be a translator coment,
+#. that will include several lines
+#: project/file1.py:8
+msgid "bar"
+msgstr ""
+
+#: project/file2.py:9
+msgid "foobar"
+msgid_plural "foobars"
+msgstr[0] ""
+msgstr[1] ""
+
+"""
+ with open(pot_file) as f:
+ actual_content = f.read()
+ assert expected_content == actual_content
+
+ @freeze_time("1994-11-11")
+ def test_extraction_with_mapping_dict(self):
+ self.dist.message_extractors = {
+ 'project': [
+ ('**/ignored/**.*', 'ignore', None),
+ ('**.py', 'python', None),
+ ],
+ }
+ self.cmd.copyright_holder = 'FooBar, Inc.'
+ self.cmd.msgid_bugs_address = 'bugs.address@email.tld'
+ self.cmd.output_file = 'project/i18n/temp.pot'
+ self.cmd.add_comments = 'TRANSLATOR:,TRANSLATORS:'
+
+ self.cmd.finalize_options()
+ self.cmd.run()
+
+ self.assert_pot_file_exists()
+
+ date = format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en')
+ expected_content = fr"""# Translations template for TestProject.
+# Copyright (C) {time.strftime('%Y')} FooBar, Inc.
+# This file is distributed under the same license as the TestProject
+# project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, {time.strftime('%Y')}.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: TestProject 0.1\n"
+"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
+"POT-Creation-Date: {date}\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel {VERSION}\n"
+
+#. TRANSLATOR: This will be a translator coment,
+#. that will include several lines
+#: project/file1.py:8
+msgid "bar"
+msgstr ""
+
+#: project/file2.py:9
+msgid "foobar"
+msgid_plural "foobars"
+msgstr[0] ""
+msgstr[1] ""
+
+"""
+ with open(pot_file) as f:
+ actual_content = f.read()
+ assert expected_content == actual_content
+
+ def test_extraction_add_location_file(self):
+ self.dist.message_extractors = {
+ 'project': [
+ ('**/ignored/**.*', 'ignore', None),
+ ('**.py', 'python', None),
+ ],
+ }
+ self.cmd.output_file = 'project/i18n/temp.pot'
+ self.cmd.add_location = 'file'
+ self.cmd.omit_header = True
+
+ self.cmd.finalize_options()
+ self.cmd.run()
+
+ self.assert_pot_file_exists()
+
+ expected_content = r"""#: project/file1.py
+msgid "bar"
+msgstr ""
+
+#: project/file2.py
+msgid "foobar"
+msgid_plural "foobars"
+msgstr[0] ""
+msgstr[1] ""
+
+"""
+ with open(pot_file) as f:
+ actual_content = f.read()
+ assert expected_content == actual_content
--- /dev/null
+#
+# Copyright (C) 2007-2011 Edgewall Software, 2013-2025 the Babel team
+# All rights reserved.
+#
+# This software is licensed as described in the file LICENSE, which
+# you should have received as part of this distribution. The terms
+# are also available at https://github.com/python-babel/babel/blob/master/LICENSE.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at https://github.com/python-babel/babel/commits/master/.
+
+from __future__ import annotations
+
+import re
+import shlex
+from functools import partial
+from io import BytesIO, StringIO
+
+import pytest
+
+from babel.messages import Catalog, extract, frontend
+from babel.messages.frontend import (
+ CommandLineInterface,
+ ExtractMessages,
+ UpdateCatalog,
+)
+from babel.messages.pofile import write_po
+from tests.messages.consts import project_dir
+from tests.messages.utils import CUSTOM_EXTRACTOR_COOKIE
+
+mapping_cfg = """
+[extractors]
+custom = tests.messages.utils:custom_extractor
+
+# Special extractor for a given Python file
+[custom: special.py]
+treat = delicious
+
+# Python source files
+[python: **.py]
+
+# Genshi templates
+[genshi: **/templates/**.html]
+include_attrs =
+
+[genshi: **/templates/**.txt]
+template_class = genshi.template:TextTemplate
+encoding = latin-1
+
+# Some custom extractor
+[custom: **/custom/*.*]
+"""
+
+mapping_toml = """
+[extractors]
+custom = "tests.messages.utils:custom_extractor"
+
+# Special extractor for a given Python file
+[[mappings]]
+method = "custom"
+pattern = "special.py"
+treat = "delightful"
+
+# Python source files
+[[mappings]]
+method = "python"
+pattern = "**.py"
+
+# Genshi templates
+[[mappings]]
+method = "genshi"
+pattern = "**/templates/**.html"
+include_attrs = ""
+
+[[mappings]]
+method = "genshi"
+pattern = "**/templates/**.txt"
+template_class = "genshi.template:TextTemplate"
+encoding = "latin-1"
+
+# Some custom extractor
+[[mappings]]
+method = "custom"
+pattern = "**/custom/*.*"
+"""
+
+
+@pytest.mark.parametrize(
+ ("data", "parser", "preprocess", "is_toml"),
+ [
+ (
+ mapping_cfg,
+ frontend.parse_mapping_cfg,
+ None,
+ False,
+ ),
+ (
+ mapping_toml,
+ frontend._parse_mapping_toml,
+ None,
+ True,
+ ),
+ (
+ mapping_toml,
+ partial(frontend._parse_mapping_toml, style="pyproject.toml"),
+ lambda s: re.sub(r"^(\[+)", r"\1tool.babel.", s, flags=re.MULTILINE),
+ True,
+ ),
+ ],
+ ids=("cfg", "toml", "pyproject-toml"),
+)
+def test_parse_mapping(data: str, parser, preprocess, is_toml):
+ if preprocess:
+ data = preprocess(data)
+ if is_toml:
+ buf = BytesIO(data.encode())
+ else:
+ buf = StringIO(data)
+
+ method_map, options_map = parser(buf)
+ assert len(method_map) == 5
+
+ assert method_map[1] == ('**.py', 'python')
+ assert options_map['**.py'] == {}
+ assert method_map[2] == ('**/templates/**.html', 'genshi')
+ assert options_map['**/templates/**.html']['include_attrs'] == ''
+ assert method_map[3] == ('**/templates/**.txt', 'genshi')
+ assert (options_map['**/templates/**.txt']['template_class']
+ == 'genshi.template:TextTemplate')
+ assert options_map['**/templates/**.txt']['encoding'] == 'latin-1'
+ assert method_map[4] == ('**/custom/*.*', 'tests.messages.utils:custom_extractor')
+ assert options_map['**/custom/*.*'] == {}
+
+
+def test_parse_keywords():
+ kw = frontend.parse_keywords(['_', 'dgettext:2',
+ 'dngettext:2,3', 'pgettext:1c,2'])
+ assert kw == {
+ '_': None,
+ 'dgettext': (2,),
+ 'dngettext': (2, 3),
+ 'pgettext': ((1, 'c'), 2),
+ }
+
+
+def test_parse_keywords_with_t():
+ kw = frontend.parse_keywords(['_:1', '_:2,2t', '_:2c,3,3t'])
+
+ assert kw == {
+ '_': {
+ None: (1,),
+ 2: (2,),
+ 3: ((2, 'c'), 3),
+ },
+ }
+
+
+def test_extract_messages_with_t():
+ content = rb"""
+_("1 arg, arg 1")
+_("2 args, arg 1", "2 args, arg 2")
+_("3 args, arg 1", "3 args, arg 2", "3 args, arg 3")
+_("4 args, arg 1", "4 args, arg 2", "4 args, arg 3", "4 args, arg 4")
+"""
+ kw = frontend.parse_keywords(['_:1', '_:2,2t', '_:2c,3,3t'])
+ result = list(extract.extract("python", BytesIO(content), kw))
+ expected = [(2, '1 arg, arg 1', [], None),
+ (3, '2 args, arg 1', [], None),
+ (3, '2 args, arg 2', [], None),
+ (4, '3 args, arg 1', [], None),
+ (4, '3 args, arg 3', [], '3 args, arg 2'),
+ (5, '4 args, arg 1', [], None)]
+ assert result == expected
+
+
+def configure_cli_command(cmdline: str | list[str]):
+ """
+ Helper to configure a command class, but not run it just yet.
+
+ :param cmdline: The command line (sans the executable name)
+ :return: Command instance
+ """
+ args = shlex.split(cmdline) if isinstance(cmdline, str) else list(cmdline)
+ cli = CommandLineInterface()
+ cmdinst = cli._configure_command(cmdname=args[0], argv=args[1:])
+ return cmdinst
+
+
+@pytest.mark.parametrize("split", (False, True))
+@pytest.mark.parametrize("arg_name", ("-k", "--keyword", "--keywords"))
+def test_extract_keyword_args_384(split, arg_name):
+ # This is a regression test for https://github.com/python-babel/babel/issues/384
+ # and it also tests that the rest of the forgotten aliases/shorthands implied by
+ # https://github.com/python-babel/babel/issues/390 are re-remembered (or rather
+ # that the mechanism for remembering them again works).
+
+ kwarg_specs = [
+ "gettext_noop",
+ "gettext_lazy",
+ "ngettext_lazy:1,2",
+ "ugettext_noop",
+ "ugettext_lazy",
+ "ungettext_lazy:1,2",
+ "pgettext_lazy:1c,2",
+ "npgettext_lazy:1c,2,3",
+ ]
+
+ if split: # Generate a command line with multiple -ks
+ kwarg_text = " ".join(f"{arg_name} {kwarg_spec}" for kwarg_spec in kwarg_specs)
+ else: # Generate a single space-separated -k
+ specs = ' '.join(kwarg_specs)
+ kwarg_text = f'{arg_name} "{specs}"'
+
+ # (Both of those invocation styles should be equivalent, so there is no parametrization from here on out)
+
+ cmdinst = configure_cli_command(
+ f"extract -F babel-django.cfg --add-comments Translators: -o django232.pot {kwarg_text} .",
+ )
+ assert isinstance(cmdinst, ExtractMessages)
+ assert set(cmdinst.keywords.keys()) == {'_', 'dgettext', 'dngettext',
+ 'gettext', 'gettext_lazy',
+ 'gettext_noop', 'N_', 'ngettext',
+ 'ngettext_lazy', 'npgettext',
+ 'npgettext_lazy', 'pgettext',
+ 'pgettext_lazy', 'ugettext',
+ 'ugettext_lazy', 'ugettext_noop',
+ 'ungettext', 'ungettext_lazy'}
+
+
+def test_update_catalog_boolean_args():
+ cmdinst = configure_cli_command(
+ "update --init-missing --no-wrap -N --ignore-obsolete --previous -i foo -o foo -l en")
+ assert isinstance(cmdinst, UpdateCatalog)
+ assert cmdinst.init_missing is True
+ assert cmdinst.no_wrap is True
+ assert cmdinst.no_fuzzy_matching is True
+ assert cmdinst.ignore_obsolete is True
+ assert cmdinst.previous is False # Mutually exclusive with no_fuzzy_matching
+
+
+
+def test_compile_catalog_dir(tmp_path):
+ """
+ Test that `compile` can compile all locales in a directory.
+ """
+ locales = ("fi_FI", "sv_SE")
+ for locale in locales:
+ l_dir = tmp_path / locale / "LC_MESSAGES"
+ l_dir.mkdir(parents=True)
+ po_file = l_dir / 'messages.po'
+ po_file.write_text('msgid "foo"\nmsgstr "bar"\n')
+ cmdinst = configure_cli_command([ # fmt: skip
+ 'compile',
+ '--statistics',
+ '--use-fuzzy',
+ '-d', str(tmp_path),
+ ])
+ assert not cmdinst.run()
+ for locale in locales:
+ assert (tmp_path / locale / "LC_MESSAGES" / "messages.mo").exists()
+
+
+def test_compile_catalog_explicit(tmp_path):
+ """
+ Test that `compile` can explicitly compile a single catalog.
+ """
+ po_file = tmp_path / 'temp.po'
+ po_file.write_text('msgid "foo"\nmsgstr "bar"\n')
+ mo_file = tmp_path / 'temp.mo'
+ cmdinst = configure_cli_command([ # fmt: skip
+ 'compile',
+ '--statistics',
+ '--use-fuzzy',
+ '-i', str(po_file),
+ '-o', str(mo_file),
+ '-l', 'fi_FI',
+ ])
+ assert not cmdinst.run()
+ assert mo_file.exists()
+
+
+
+@pytest.mark.parametrize("explicit_locale", (None, 'fi_FI'), ids=("implicit", "explicit"))
+def test_update_dir(tmp_path, explicit_locale: bool):
+ """
+ Test that `update` can deal with directories too.
+ """
+ template = Catalog()
+ template.add("1")
+ template.add("2")
+ template.add("3")
+ tmpl_file = (tmp_path / 'temp-template.pot')
+ with tmpl_file.open("wb") as outfp:
+ write_po(outfp, template)
+ locales = ("fi_FI", "sv_SE")
+ for locale in locales:
+ l_dir = tmp_path / locale / "LC_MESSAGES"
+ l_dir.mkdir(parents=True)
+ po_file = l_dir / 'messages.po'
+ po_file.touch()
+ cmdinst = configure_cli_command([ # fmt: skip
+ 'update',
+ '-i', str(tmpl_file),
+ '-d', str(tmp_path),
+ *(['-l', explicit_locale] if explicit_locale else []),
+ ])
+ assert not cmdinst.run()
+ for locale in locales:
+ if explicit_locale and locale != explicit_locale:
+ continue
+ assert (tmp_path / locale / "LC_MESSAGES" / "messages.po").stat().st_size > 0
+
+
+def test_extract_cli_knows_dash_s():
+ # This is a regression test for https://github.com/python-babel/babel/issues/390
+ cmdinst = configure_cli_command("extract -s -o foo babel")
+ assert isinstance(cmdinst, ExtractMessages)
+ assert cmdinst.strip_comments
+
+
+def test_extract_cli_knows_dash_dash_last_dash_translator():
+ cmdinst = configure_cli_command('extract --last-translator "FULL NAME EMAIL@ADDRESS" -o foo babel')
+ assert isinstance(cmdinst, ExtractMessages)
+ assert cmdinst.last_translator == "FULL NAME EMAIL@ADDRESS"
+
+
+def test_extract_add_location():
+ cmdinst = configure_cli_command("extract -o foo babel --add-location full")
+ assert isinstance(cmdinst, ExtractMessages)
+ assert cmdinst.add_location == 'full'
+ assert not cmdinst.no_location
+ assert cmdinst.include_lineno
+
+ cmdinst = configure_cli_command("extract -o foo babel --add-location file")
+ assert isinstance(cmdinst, ExtractMessages)
+ assert cmdinst.add_location == 'file'
+ assert not cmdinst.no_location
+ assert not cmdinst.include_lineno
+
+ cmdinst = configure_cli_command("extract -o foo babel --add-location never")
+ assert isinstance(cmdinst, ExtractMessages)
+ assert cmdinst.add_location == 'never'
+ assert cmdinst.no_location
+
+
+def test_extract_error_code(monkeypatch, capsys):
+ monkeypatch.chdir(project_dir)
+ cmdinst = configure_cli_command("compile --domain=messages --directory i18n --locale fi_BUGGY")
+ assert cmdinst.run() == 1
+ out, err = capsys.readouterr()
+ if err:
+ assert "unknown named placeholder 'merkki'" in err
+
+
+@pytest.mark.parametrize("with_underscore_ignore", (False, True))
+def test_extract_ignore_dirs(monkeypatch, capsys, tmp_path, with_underscore_ignore):
+ pot_file = tmp_path / 'temp.pot'
+ monkeypatch.chdir(project_dir)
+ cmd = f"extract . -o '{pot_file}' --ignore-dirs '*ignored* .*' "
+ if with_underscore_ignore:
+ # This also tests that multiple arguments are supported.
+ cmd += "--ignore-dirs '_*'"
+ cmdinst = configure_cli_command(cmd)
+ assert isinstance(cmdinst, ExtractMessages)
+ assert cmdinst.directory_filter
+ cmdinst.run()
+ pot_content = pot_file.read_text()
+
+ # The `ignored` directory is now actually ignored:
+ assert 'this_wont_normally_be_here' not in pot_content
+
+ # Since we manually set a filter, the otherwise `_hidden` directory is walked into,
+ # unless we opt in to ignore it again
+ assert ('ssshhh....' in pot_content) != with_underscore_ignore
+ assert ('_hidden_by_default' in pot_content) != with_underscore_ignore
+
+
+def test_extract_header_comment(monkeypatch, tmp_path):
+ pot_file = tmp_path / 'temp.pot'
+ monkeypatch.chdir(project_dir)
+ cmdinst = configure_cli_command(f"extract . -o '{pot_file}' --header-comment 'Boing' ")
+ cmdinst.run()
+ pot_content = pot_file.read_text()
+ assert 'Boing' in pot_content
+
+
+@pytest.mark.parametrize("mapping_format", ("toml", "cfg"))
+def test_pr_1121(tmp_path, monkeypatch, caplog, mapping_format):
+ """
+ Test that extraction uses the first matching method and options,
+ instead of the first matching method and last matching options.
+
+ Without the fix in PR #1121, this test would fail,
+ since the `custom_extractor` isn't passed a delicious treat via
+ the configuration.
+ """
+ if mapping_format == "cfg":
+ mapping_file = (tmp_path / "mapping.cfg")
+ mapping_file.write_text(mapping_cfg)
+ else:
+ mapping_file = (tmp_path / "mapping.toml")
+ mapping_file.write_text(mapping_toml)
+ (tmp_path / "special.py").write_text("# this file is special")
+ pot_path = (tmp_path / "output.pot")
+ monkeypatch.chdir(tmp_path)
+ cmdinst = configure_cli_command(f"extract . -o {shlex.quote(str(pot_path))} --mapping {shlex.quote(mapping_file.name)}")
+ assert isinstance(cmdinst, ExtractMessages)
+ cmdinst.run()
+ # If the custom extractor didn't run, we wouldn't see the cookie in there.
+ assert CUSTOM_EXTRACTOR_COOKIE in pot_path.read_text()
--- /dev/null
+#
+# Copyright (C) 2007-2011 Edgewall Software, 2013-2025 the Babel team
+# All rights reserved.
+#
+# This software is licensed as described in the file LICENSE, which
+# you should have received as part of this distribution. The terms
+# are also available at https://github.com/python-babel/babel/blob/master/LICENSE.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at https://github.com/python-babel/babel/commits/master/.
+
+from __future__ import annotations
+
+import os
+import shutil
+import unittest
+from datetime import datetime
+
+import pytest
+from freezegun import freeze_time
+
+from babel import __version__ as VERSION
+from babel.dates import format_datetime
+from babel.messages import frontend
+from babel.util import LOCALTZ
+from tests.messages.consts import (
+ TEST_PROJECT_DISTRIBUTION_DATA,
+ data_dir,
+ get_po_file_path,
+ i18n_dir,
+)
+from tests.messages.utils import Distribution
+
+
+class InitCatalogTestCase(unittest.TestCase):
+
+ def setUp(self):
+ self.olddir = os.getcwd()
+ os.chdir(data_dir)
+
+ self.dist = Distribution(TEST_PROJECT_DISTRIBUTION_DATA)
+ self.cmd = frontend.InitCatalog(self.dist)
+ self.cmd.initialize_options()
+
+ def tearDown(self):
+ for dirname in ['en_US', 'ja_JP', 'lv_LV']:
+ locale_dir = os.path.join(i18n_dir, dirname)
+ if os.path.isdir(locale_dir):
+ shutil.rmtree(locale_dir)
+
+ os.chdir(self.olddir)
+
+ def test_no_input_file(self):
+ self.cmd.locale = 'en_US'
+ self.cmd.output_file = 'dummy'
+ with pytest.raises(frontend.OptionError):
+ self.cmd.finalize_options()
+
+ def test_no_locale(self):
+ self.cmd.input_file = 'dummy'
+ self.cmd.output_file = 'dummy'
+ with pytest.raises(frontend.OptionError):
+ self.cmd.finalize_options()
+
+ @freeze_time("1994-11-11")
+ def test_with_output_dir(self):
+ self.cmd.input_file = 'project/i18n/messages.pot'
+ self.cmd.locale = 'en_US'
+ self.cmd.output_dir = 'project/i18n'
+
+ self.cmd.finalize_options()
+ self.cmd.run()
+
+ po_file = get_po_file_path('en_US')
+ assert os.path.isfile(po_file)
+
+ date = format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en')
+ expected_content = fr"""# English (United States) translations for TestProject.
+# Copyright (C) 2007 FooBar, Inc.
+# This file is distributed under the same license as the TestProject
+# project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: TestProject 0.1\n"
+"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
+"POT-Creation-Date: 2007-04-01 15:30+0200\n"
+"PO-Revision-Date: {date}\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language: en_US\n"
+"Language-Team: en_US <LL@li.org>\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel {VERSION}\n"
+
+#. This will be a translator coment,
+#. that will include several lines
+#: project/file1.py:8
+msgid "bar"
+msgstr ""
+
+#: project/file2.py:9
+msgid "foobar"
+msgid_plural "foobars"
+msgstr[0] ""
+msgstr[1] ""
+
+"""
+ with open(po_file) as f:
+ actual_content = f.read()
+ assert expected_content == actual_content
+
+ @freeze_time("1994-11-11")
+ def test_keeps_catalog_non_fuzzy(self):
+ self.cmd.input_file = 'project/i18n/messages_non_fuzzy.pot'
+ self.cmd.locale = 'en_US'
+ self.cmd.output_dir = 'project/i18n'
+
+ self.cmd.finalize_options()
+ self.cmd.run()
+
+ po_file = get_po_file_path('en_US')
+ assert os.path.isfile(po_file)
+
+ date = format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en')
+ expected_content = fr"""# English (United States) translations for TestProject.
+# Copyright (C) 2007 FooBar, Inc.
+# This file is distributed under the same license as the TestProject
+# project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: TestProject 0.1\n"
+"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
+"POT-Creation-Date: 2007-04-01 15:30+0200\n"
+"PO-Revision-Date: {date}\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language: en_US\n"
+"Language-Team: en_US <LL@li.org>\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel {VERSION}\n"
+
+#. This will be a translator coment,
+#. that will include several lines
+#: project/file1.py:8
+msgid "bar"
+msgstr ""
+
+#: project/file2.py:9
+msgid "foobar"
+msgid_plural "foobars"
+msgstr[0] ""
+msgstr[1] ""
+
+"""
+ with open(po_file) as f:
+ actual_content = f.read()
+ assert expected_content == actual_content
+
+ @freeze_time("1994-11-11")
+ def test_correct_init_more_than_2_plurals(self):
+ self.cmd.input_file = 'project/i18n/messages.pot'
+ self.cmd.locale = 'lv_LV'
+ self.cmd.output_dir = 'project/i18n'
+
+ self.cmd.finalize_options()
+ self.cmd.run()
+
+ po_file = get_po_file_path('lv_LV')
+ assert os.path.isfile(po_file)
+
+ date = format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en')
+ expected_content = fr"""# Latvian (Latvia) translations for TestProject.
+# Copyright (C) 2007 FooBar, Inc.
+# This file is distributed under the same license as the TestProject
+# project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: TestProject 0.1\n"
+"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
+"POT-Creation-Date: 2007-04-01 15:30+0200\n"
+"PO-Revision-Date: {date}\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language: lv_LV\n"
+"Language-Team: lv_LV <LL@li.org>\n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 :"
+" 2);\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel {VERSION}\n"
+
+#. This will be a translator coment,
+#. that will include several lines
+#: project/file1.py:8
+msgid "bar"
+msgstr ""
+
+#: project/file2.py:9
+msgid "foobar"
+msgid_plural "foobars"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+
+"""
+ with open(po_file) as f:
+ actual_content = f.read()
+ assert expected_content == actual_content
+
+ @freeze_time("1994-11-11")
+ def test_correct_init_singular_plural_forms(self):
+ self.cmd.input_file = 'project/i18n/messages.pot'
+ self.cmd.locale = 'ja_JP'
+ self.cmd.output_dir = 'project/i18n'
+
+ self.cmd.finalize_options()
+ self.cmd.run()
+
+ po_file = get_po_file_path('ja_JP')
+ assert os.path.isfile(po_file)
+
+ date = format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='ja_JP')
+ expected_content = fr"""# Japanese (Japan) translations for TestProject.
+# Copyright (C) 2007 FooBar, Inc.
+# This file is distributed under the same license as the TestProject
+# project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: TestProject 0.1\n"
+"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
+"POT-Creation-Date: 2007-04-01 15:30+0200\n"
+"PO-Revision-Date: {date}\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language: ja_JP\n"
+"Language-Team: ja_JP <LL@li.org>\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel {VERSION}\n"
+
+#. This will be a translator coment,
+#. that will include several lines
+#: project/file1.py:8
+msgid "bar"
+msgstr ""
+
+#: project/file2.py:9
+msgid "foobar"
+msgid_plural "foobars"
+msgstr[0] ""
+
+"""
+ with open(po_file) as f:
+ actual_content = f.read()
+ assert expected_content == actual_content
+
+ @freeze_time("1994-11-11")
+ def test_supports_no_wrap(self):
+ self.cmd.input_file = 'project/i18n/long_messages.pot'
+ self.cmd.locale = 'en_US'
+ self.cmd.output_dir = 'project/i18n'
+
+ long_message = '"' + 'xxxxx ' * 15 + '"'
+
+ with open('project/i18n/messages.pot', 'rb') as f:
+ pot_contents = f.read().decode('latin-1')
+ pot_with_very_long_line = pot_contents.replace('"bar"', long_message)
+ with open(self.cmd.input_file, 'wb') as f:
+ f.write(pot_with_very_long_line.encode('latin-1'))
+ self.cmd.no_wrap = True
+
+ self.cmd.finalize_options()
+ self.cmd.run()
+
+ po_file = get_po_file_path('en_US')
+ assert os.path.isfile(po_file)
+ date = format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en_US')
+ expected_content = fr"""# English (United States) translations for TestProject.
+# Copyright (C) 2007 FooBar, Inc.
+# This file is distributed under the same license as the TestProject
+# project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: TestProject 0.1\n"
+"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
+"POT-Creation-Date: 2007-04-01 15:30+0200\n"
+"PO-Revision-Date: {date}\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language: en_US\n"
+"Language-Team: en_US <LL@li.org>\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel {VERSION}\n"
+
+#. This will be a translator coment,
+#. that will include several lines
+#: project/file1.py:8
+msgid {long_message}
+msgstr ""
+
+#: project/file2.py:9
+msgid "foobar"
+msgid_plural "foobars"
+msgstr[0] ""
+msgstr[1] ""
+
+"""
+ with open(po_file) as f:
+ actual_content = f.read()
+ assert expected_content == actual_content
+
+ @freeze_time("1994-11-11")
+ def test_supports_width(self):
+ self.cmd.input_file = 'project/i18n/long_messages.pot'
+ self.cmd.locale = 'en_US'
+ self.cmd.output_dir = 'project/i18n'
+
+ long_message = '"' + 'xxxxx ' * 15 + '"'
+
+ with open('project/i18n/messages.pot', 'rb') as f:
+ pot_contents = f.read().decode('latin-1')
+ pot_with_very_long_line = pot_contents.replace('"bar"', long_message)
+ with open(self.cmd.input_file, 'wb') as f:
+ f.write(pot_with_very_long_line.encode('latin-1'))
+ self.cmd.width = 120
+ self.cmd.finalize_options()
+ self.cmd.run()
+
+ po_file = get_po_file_path('en_US')
+ assert os.path.isfile(po_file)
+ date = format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en_US')
+ expected_content = fr"""# English (United States) translations for TestProject.
+# Copyright (C) 2007 FooBar, Inc.
+# This file is distributed under the same license as the TestProject
+# project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: TestProject 0.1\n"
+"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
+"POT-Creation-Date: 2007-04-01 15:30+0200\n"
+"PO-Revision-Date: {date}\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language: en_US\n"
+"Language-Team: en_US <LL@li.org>\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel {VERSION}\n"
+
+#. This will be a translator coment,
+#. that will include several lines
+#: project/file1.py:8
+msgid {long_message}
+msgstr ""
+
+#: project/file2.py:9
+msgid "foobar"
+msgid_plural "foobars"
+msgstr[0] ""
+msgstr[1] ""
+
+"""
+ with open(po_file) as f:
+ actual_content = f.read()
+ assert expected_content == actual_content
import copy
import datetime
import pickle
-import unittest
from io import StringIO
from babel.dates import UTC, format_datetime
from babel.util import FixedOffsetTimezone
-class MessageTestCase(unittest.TestCase):
-
- def test_python_format(self):
- assert catalog.PYTHON_FORMAT.search('foo %d bar')
- assert catalog.PYTHON_FORMAT.search('foo %s bar')
- assert catalog.PYTHON_FORMAT.search('foo %r bar')
- assert catalog.PYTHON_FORMAT.search('foo %(name).1f')
- assert catalog.PYTHON_FORMAT.search('foo %(name)3.3f')
- assert catalog.PYTHON_FORMAT.search('foo %(name)3f')
- assert catalog.PYTHON_FORMAT.search('foo %(name)06d')
- assert catalog.PYTHON_FORMAT.search('foo %(name)Li')
- assert catalog.PYTHON_FORMAT.search('foo %(name)#d')
- assert catalog.PYTHON_FORMAT.search('foo %(name)-4.4hs')
- assert catalog.PYTHON_FORMAT.search('foo %(name)*.3f')
- assert catalog.PYTHON_FORMAT.search('foo %(name).*f')
- assert catalog.PYTHON_FORMAT.search('foo %(name)3.*f')
- assert catalog.PYTHON_FORMAT.search('foo %(name)*.*f')
- assert catalog.PYTHON_FORMAT.search('foo %()s')
-
- def test_python_brace_format(self):
- assert not catalog._has_python_brace_format('')
- assert not catalog._has_python_brace_format('foo')
- assert not catalog._has_python_brace_format('{')
- assert not catalog._has_python_brace_format('}')
- assert not catalog._has_python_brace_format('{} {')
- assert not catalog._has_python_brace_format('{{}}')
- assert catalog._has_python_brace_format('{}')
- assert catalog._has_python_brace_format('foo {name}')
- assert catalog._has_python_brace_format('foo {name!s}')
- assert catalog._has_python_brace_format('foo {name!r}')
- assert catalog._has_python_brace_format('foo {name!a}')
- assert catalog._has_python_brace_format('foo {name!r:10}')
- assert catalog._has_python_brace_format('foo {name!r:10.2}')
- assert catalog._has_python_brace_format('foo {name!r:10.2f}')
- assert catalog._has_python_brace_format('foo {name!r:10.2f} {name!r:10.2f}')
- assert catalog._has_python_brace_format('foo {name!r:10.2f=}')
-
- def test_translator_comments(self):
- mess = catalog.Message('foo', user_comments=['Comment About `foo`'])
- assert mess.user_comments == ['Comment About `foo`']
- mess = catalog.Message('foo',
- auto_comments=['Comment 1 About `foo`',
- 'Comment 2 About `foo`'])
- assert mess.auto_comments == ['Comment 1 About `foo`', 'Comment 2 About `foo`']
-
- def test_clone_message_object(self):
- msg = catalog.Message('foo', locations=[('foo.py', 42)])
- clone = msg.clone()
- clone.locations.append(('bar.py', 42))
- assert msg.locations == [('foo.py', 42)]
- msg.flags.add('fuzzy')
- assert not clone.fuzzy and msg.fuzzy
-
-
-class CatalogTestCase(unittest.TestCase):
-
- def test_add_returns_message_instance(self):
- cat = catalog.Catalog()
- message = cat.add('foo')
- assert message.id == 'foo'
-
- def test_two_messages_with_same_singular(self):
- cat = catalog.Catalog()
- cat.add('foo')
- cat.add(('foo', 'foos'))
- assert len(cat) == 1
-
- def test_duplicate_auto_comment(self):
- cat = catalog.Catalog()
- cat.add('foo', auto_comments=['A comment'])
- cat.add('foo', auto_comments=['A comment', 'Another comment'])
- assert cat['foo'].auto_comments == ['A comment', 'Another comment']
-
- def test_duplicate_user_comment(self):
- cat = catalog.Catalog()
- cat.add('foo', user_comments=['A comment'])
- cat.add('foo', user_comments=['A comment', 'Another comment'])
- assert cat['foo'].user_comments == ['A comment', 'Another comment']
-
- def test_duplicate_location(self):
- cat = catalog.Catalog()
- cat.add('foo', locations=[('foo.py', 1)])
- cat.add('foo', locations=[('foo.py', 1)])
- assert cat['foo'].locations == [('foo.py', 1)]
-
- def test_update_message_changed_to_plural(self):
- cat = catalog.Catalog()
- cat.add('foo', 'Voh')
- tmpl = catalog.Catalog()
- tmpl.add(('foo', 'foos'))
- cat.update(tmpl)
- assert cat['foo'].string == ('Voh', '')
- assert cat['foo'].fuzzy
-
- def test_update_message_changed_to_simple(self):
- cat = catalog.Catalog()
- cat.add('foo' 'foos', ('Voh', 'Vöhs'))
- tmpl = catalog.Catalog()
- tmpl.add('foo')
- cat.update(tmpl)
- assert cat['foo'].string == 'Voh'
- assert cat['foo'].fuzzy
-
- def test_update_message_updates_comments(self):
- cat = catalog.Catalog()
- cat['foo'] = catalog.Message('foo', locations=[('main.py', 5)])
- assert cat['foo'].auto_comments == []
- assert cat['foo'].user_comments == []
- # Update cat['foo'] with a new location and a comment
- cat['foo'] = catalog.Message('foo', locations=[('main.py', 7)],
- user_comments=['Foo Bar comment 1'])
- assert cat['foo'].user_comments == ['Foo Bar comment 1']
- # now add yet another location with another comment
- cat['foo'] = catalog.Message('foo', locations=[('main.py', 9)],
- auto_comments=['Foo Bar comment 2'])
- assert cat['foo'].auto_comments == ['Foo Bar comment 2']
-
- def test_update_fuzzy_matching_with_case_change(self):
- cat = catalog.Catalog()
- cat.add('FOO', 'Voh')
- cat.add('bar', 'Bahr')
- tmpl = catalog.Catalog()
- tmpl.add('foo')
- cat.update(tmpl)
- assert len(cat.obsolete) == 1
- assert 'FOO' not in cat
-
- assert cat['foo'].string == 'Voh'
- assert cat['foo'].fuzzy is True
-
- def test_update_fuzzy_matching_with_char_change(self):
- cat = catalog.Catalog()
- cat.add('fo', 'Voh')
- cat.add('bar', 'Bahr')
- tmpl = catalog.Catalog()
- tmpl.add('foo')
- cat.update(tmpl)
- assert len(cat.obsolete) == 1
- assert 'fo' not in cat
-
- assert cat['foo'].string == 'Voh'
- assert cat['foo'].fuzzy is True
-
- def test_update_fuzzy_matching_no_msgstr(self):
- cat = catalog.Catalog()
- cat.add('fo', '')
- tmpl = catalog.Catalog()
- tmpl.add('fo')
- tmpl.add('foo')
- cat.update(tmpl)
- assert 'fo' in cat
- assert 'foo' in cat
-
- assert cat['fo'].string == ''
- assert cat['fo'].fuzzy is False
- assert cat['foo'].string is None
- assert cat['foo'].fuzzy is False
-
- def test_update_fuzzy_matching_with_new_context(self):
- cat = catalog.Catalog()
- cat.add('foo', 'Voh')
- cat.add('bar', 'Bahr')
- tmpl = catalog.Catalog()
- tmpl.add('Foo', context='Menu')
- cat.update(tmpl)
- assert len(cat.obsolete) == 1
- assert 'foo' not in cat
-
- message = cat.get('Foo', 'Menu')
- assert message.string == 'Voh'
- assert message.fuzzy is True
- assert message.context == 'Menu'
-
- def test_update_fuzzy_matching_with_changed_context(self):
- cat = catalog.Catalog()
- cat.add('foo', 'Voh', context='Menu|File')
- cat.add('bar', 'Bahr', context='Menu|File')
- tmpl = catalog.Catalog()
- tmpl.add('Foo', context='Menu|Edit')
- cat.update(tmpl)
- assert len(cat.obsolete) == 1
- assert cat.get('Foo', 'Menu|File') is None
-
- message = cat.get('Foo', 'Menu|Edit')
- assert message.string == 'Voh'
- assert message.fuzzy is True
- assert message.context == 'Menu|Edit'
-
- def test_update_fuzzy_matching_no_cascading(self):
- cat = catalog.Catalog()
- cat.add('fo', 'Voh')
- cat.add('foo', 'Vohe')
- tmpl = catalog.Catalog()
- tmpl.add('fo')
- tmpl.add('foo')
- tmpl.add('fooo')
- cat.update(tmpl)
- assert 'fo' in cat
- assert 'foo' in cat
-
- assert cat['fo'].string == 'Voh'
- assert cat['fo'].fuzzy is False
- assert cat['foo'].string == 'Vohe'
- assert cat['foo'].fuzzy is False
- assert cat['fooo'].string == 'Vohe'
- assert cat['fooo'].fuzzy is True
-
- def test_update_fuzzy_matching_long_string(self):
- lipsum = "\
+def test_message_python_format():
+ assert catalog.PYTHON_FORMAT.search('foo %d bar')
+ assert catalog.PYTHON_FORMAT.search('foo %s bar')
+ assert catalog.PYTHON_FORMAT.search('foo %r bar')
+ assert catalog.PYTHON_FORMAT.search('foo %(name).1f')
+ assert catalog.PYTHON_FORMAT.search('foo %(name)3.3f')
+ assert catalog.PYTHON_FORMAT.search('foo %(name)3f')
+ assert catalog.PYTHON_FORMAT.search('foo %(name)06d')
+ assert catalog.PYTHON_FORMAT.search('foo %(name)Li')
+ assert catalog.PYTHON_FORMAT.search('foo %(name)#d')
+ assert catalog.PYTHON_FORMAT.search('foo %(name)-4.4hs')
+ assert catalog.PYTHON_FORMAT.search('foo %(name)*.3f')
+ assert catalog.PYTHON_FORMAT.search('foo %(name).*f')
+ assert catalog.PYTHON_FORMAT.search('foo %(name)3.*f')
+ assert catalog.PYTHON_FORMAT.search('foo %(name)*.*f')
+ assert catalog.PYTHON_FORMAT.search('foo %()s')
+
+
+def test_message_python_brace_format():
+ assert not catalog._has_python_brace_format('')
+ assert not catalog._has_python_brace_format('foo')
+ assert not catalog._has_python_brace_format('{')
+ assert not catalog._has_python_brace_format('}')
+ assert not catalog._has_python_brace_format('{} {')
+ assert not catalog._has_python_brace_format('{{}}')
+ assert catalog._has_python_brace_format('{}')
+ assert catalog._has_python_brace_format('foo {name}')
+ assert catalog._has_python_brace_format('foo {name!s}')
+ assert catalog._has_python_brace_format('foo {name!r}')
+ assert catalog._has_python_brace_format('foo {name!a}')
+ assert catalog._has_python_brace_format('foo {name!r:10}')
+ assert catalog._has_python_brace_format('foo {name!r:10.2}')
+ assert catalog._has_python_brace_format('foo {name!r:10.2f}')
+ assert catalog._has_python_brace_format('foo {name!r:10.2f} {name!r:10.2f}')
+ assert catalog._has_python_brace_format('foo {name!r:10.2f=}')
+
+
+def test_message_translator_comments():
+ mess = catalog.Message('foo', user_comments=['Comment About `foo`'])
+ assert mess.user_comments == ['Comment About `foo`']
+ mess = catalog.Message('foo',
+ auto_comments=['Comment 1 About `foo`',
+ 'Comment 2 About `foo`'])
+ assert mess.auto_comments == ['Comment 1 About `foo`', 'Comment 2 About `foo`']
+
+
+def test_message_clone_message_object():
+ msg = catalog.Message('foo', locations=[('foo.py', 42)])
+ clone = msg.clone()
+ clone.locations.append(('bar.py', 42))
+ assert msg.locations == [('foo.py', 42)]
+ msg.flags.add('fuzzy')
+ assert not clone.fuzzy and msg.fuzzy
+
+
+def test_catalog_add_returns_message_instance():
+ cat = catalog.Catalog()
+ message = cat.add('foo')
+ assert message.id == 'foo'
+
+
+def test_catalog_two_messages_with_same_singular():
+ cat = catalog.Catalog()
+ cat.add('foo')
+ cat.add(('foo', 'foos'))
+ assert len(cat) == 1
+
+
+def test_catalog_duplicate_auto_comment():
+ cat = catalog.Catalog()
+ cat.add('foo', auto_comments=['A comment'])
+ cat.add('foo', auto_comments=['A comment', 'Another comment'])
+ assert cat['foo'].auto_comments == ['A comment', 'Another comment']
+
+
+def test_catalog_duplicate_user_comment():
+ cat = catalog.Catalog()
+ cat.add('foo', user_comments=['A comment'])
+ cat.add('foo', user_comments=['A comment', 'Another comment'])
+ assert cat['foo'].user_comments == ['A comment', 'Another comment']
+
+
+def test_catalog_duplicate_location():
+ cat = catalog.Catalog()
+ cat.add('foo', locations=[('foo.py', 1)])
+ cat.add('foo', locations=[('foo.py', 1)])
+ assert cat['foo'].locations == [('foo.py', 1)]
+
+
+def test_catalog_update_message_changed_to_plural():
+ cat = catalog.Catalog()
+ cat.add('foo', 'Voh')
+ tmpl = catalog.Catalog()
+ tmpl.add(('foo', 'foos'))
+ cat.update(tmpl)
+ assert cat['foo'].string == ('Voh', '')
+ assert cat['foo'].fuzzy
+
+
+def test_catalog_update_message_changed_to_simple():
+ cat = catalog.Catalog()
+ cat.add('foo' 'foos', ('Voh', 'Vöhs'))
+ tmpl = catalog.Catalog()
+ tmpl.add('foo')
+ cat.update(tmpl)
+ assert cat['foo'].string == 'Voh'
+ assert cat['foo'].fuzzy
+
+
+def test_catalog_update_message_updates_comments():
+ cat = catalog.Catalog()
+ cat['foo'] = catalog.Message('foo', locations=[('main.py', 5)])
+ assert cat['foo'].auto_comments == []
+ assert cat['foo'].user_comments == []
+ # Update cat['foo'] with a new location and a comment
+ cat['foo'] = catalog.Message('foo', locations=[('main.py', 7)],
+ user_comments=['Foo Bar comment 1'])
+ assert cat['foo'].user_comments == ['Foo Bar comment 1']
+ # now add yet another location with another comment
+ cat['foo'] = catalog.Message('foo', locations=[('main.py', 9)],
+ auto_comments=['Foo Bar comment 2'])
+ assert cat['foo'].auto_comments == ['Foo Bar comment 2']
+
+
+def test_catalog_update_fuzzy_matching_with_case_change():
+ cat = catalog.Catalog()
+ cat.add('FOO', 'Voh')
+ cat.add('bar', 'Bahr')
+ tmpl = catalog.Catalog()
+ tmpl.add('foo')
+ cat.update(tmpl)
+ assert len(cat.obsolete) == 1
+ assert 'FOO' not in cat
+
+ assert cat['foo'].string == 'Voh'
+ assert cat['foo'].fuzzy is True
+
+
+def test_catalog_update_fuzzy_matching_with_char_change():
+ cat = catalog.Catalog()
+ cat.add('fo', 'Voh')
+ cat.add('bar', 'Bahr')
+ tmpl = catalog.Catalog()
+ tmpl.add('foo')
+ cat.update(tmpl)
+ assert len(cat.obsolete) == 1
+ assert 'fo' not in cat
+
+ assert cat['foo'].string == 'Voh'
+ assert cat['foo'].fuzzy is True
+
+
+def test_catalog_update_fuzzy_matching_no_msgstr():
+ cat = catalog.Catalog()
+ cat.add('fo', '')
+ tmpl = catalog.Catalog()
+ tmpl.add('fo')
+ tmpl.add('foo')
+ cat.update(tmpl)
+ assert 'fo' in cat
+ assert 'foo' in cat
+
+ assert cat['fo'].string == ''
+ assert cat['fo'].fuzzy is False
+ assert cat['foo'].string is None
+ assert cat['foo'].fuzzy is False
+
+
+def test_catalog_update_fuzzy_matching_with_new_context():
+ cat = catalog.Catalog()
+ cat.add('foo', 'Voh')
+ cat.add('bar', 'Bahr')
+ tmpl = catalog.Catalog()
+ tmpl.add('Foo', context='Menu')
+ cat.update(tmpl)
+ assert len(cat.obsolete) == 1
+ assert 'foo' not in cat
+
+ message = cat.get('Foo', 'Menu')
+ assert message.string == 'Voh'
+ assert message.fuzzy is True
+ assert message.context == 'Menu'
+
+
+def test_catalog_update_fuzzy_matching_with_changed_context():
+ cat = catalog.Catalog()
+ cat.add('foo', 'Voh', context='Menu|File')
+ cat.add('bar', 'Bahr', context='Menu|File')
+ tmpl = catalog.Catalog()
+ tmpl.add('Foo', context='Menu|Edit')
+ cat.update(tmpl)
+ assert len(cat.obsolete) == 1
+ assert cat.get('Foo', 'Menu|File') is None
+
+ message = cat.get('Foo', 'Menu|Edit')
+ assert message.string == 'Voh'
+ assert message.fuzzy is True
+ assert message.context == 'Menu|Edit'
+
+
+def test_catalog_update_fuzzy_matching_no_cascading():
+ cat = catalog.Catalog()
+ cat.add('fo', 'Voh')
+ cat.add('foo', 'Vohe')
+ tmpl = catalog.Catalog()
+ tmpl.add('fo')
+ tmpl.add('foo')
+ tmpl.add('fooo')
+ cat.update(tmpl)
+ assert 'fo' in cat
+ assert 'foo' in cat
+
+ assert cat['fo'].string == 'Voh'
+ assert cat['fo'].fuzzy is False
+ assert cat['foo'].string == 'Vohe'
+ assert cat['foo'].fuzzy is False
+ assert cat['fooo'].string == 'Vohe'
+ assert cat['fooo'].fuzzy is True
+
+
+def test_catalog_update_fuzzy_matching_long_string():
+ lipsum = "\
Lorem Ipsum is simply dummy text of the printing and typesetting \
industry. Lorem Ipsum has been the industry's standard dummy text ever \
since the 1500s, when an unknown printer took a galley of type and \
the release of Letraset sheets containing Lorem Ipsum passages, and \
more recently with desktop publishing software like Aldus PageMaker \
including versions of Lorem Ipsum."
- cat = catalog.Catalog()
- cat.add("ZZZZZZ " + lipsum, "foo")
- tmpl = catalog.Catalog()
- tmpl.add(lipsum + " ZZZZZZ")
- cat.update(tmpl)
- assert cat[lipsum + " ZZZZZZ"].fuzzy is True
- assert len(cat.obsolete) == 0
-
- def test_update_without_fuzzy_matching(self):
- cat = catalog.Catalog()
- cat.add('fo', 'Voh')
- cat.add('bar', 'Bahr')
- tmpl = catalog.Catalog()
- tmpl.add('foo')
- cat.update(tmpl, no_fuzzy_matching=True)
- assert len(cat.obsolete) == 2
-
- def test_fuzzy_matching_regarding_plurals(self):
- cat = catalog.Catalog()
- cat.add(('foo', 'foh'), ('foo', 'foh'))
- ru = copy.copy(cat)
- ru.locale = 'ru_RU'
- ru.update(cat)
- assert ru['foo'].fuzzy is True
- ru = copy.copy(cat)
- ru.locale = 'ru_RU'
- ru['foo'].string = ('foh', 'fohh', 'fohhh')
- ru.update(cat)
- assert ru['foo'].fuzzy is False
-
- def test_update_no_template_mutation(self):
- tmpl = catalog.Catalog()
- tmpl.add('foo')
- cat1 = catalog.Catalog()
- cat1.add('foo', 'Voh')
- cat1.update(tmpl)
- cat2 = catalog.Catalog()
- cat2.update(tmpl)
-
- assert cat2['foo'].string is None
- assert cat2['foo'].fuzzy is False
-
- def test_update_po_updates_pot_creation_date(self):
- template = catalog.Catalog()
- localized_catalog = copy.deepcopy(template)
- localized_catalog.locale = 'de_DE'
- assert template.mime_headers != localized_catalog.mime_headers
- assert template.creation_date == localized_catalog.creation_date
- template.creation_date = datetime.datetime.now() - \
- datetime.timedelta(minutes=5)
- localized_catalog.update(template)
- assert template.creation_date == localized_catalog.creation_date
-
- def test_update_po_ignores_pot_creation_date(self):
- template = catalog.Catalog()
- localized_catalog = copy.deepcopy(template)
- localized_catalog.locale = 'de_DE'
- assert template.mime_headers != localized_catalog.mime_headers
- assert template.creation_date == localized_catalog.creation_date
- template.creation_date = datetime.datetime.now() - \
- datetime.timedelta(minutes=5)
- localized_catalog.update(template, update_creation_date=False)
- assert template.creation_date != localized_catalog.creation_date
-
- def test_update_po_keeps_po_revision_date(self):
- template = catalog.Catalog()
- localized_catalog = copy.deepcopy(template)
- localized_catalog.locale = 'de_DE'
- fake_rev_date = datetime.datetime.now() - datetime.timedelta(days=5)
- localized_catalog.revision_date = fake_rev_date
- assert template.mime_headers != localized_catalog.mime_headers
- assert template.creation_date == localized_catalog.creation_date
- template.creation_date = datetime.datetime.now() - \
- datetime.timedelta(minutes=5)
- localized_catalog.update(template)
- assert localized_catalog.revision_date == fake_rev_date
-
- def test_stores_datetime_correctly(self):
- localized = catalog.Catalog()
- localized.locale = 'de_DE'
- localized[''] = catalog.Message('',
- "POT-Creation-Date: 2009-03-09 15:47-0700\n" +
- "PO-Revision-Date: 2009-03-09 15:47-0700\n")
- for key, value in localized.mime_headers:
- if key in ('POT-Creation-Date', 'PO-Revision-Date'):
- assert value == '2009-03-09 15:47-0700'
-
- def test_mime_headers_contain_same_information_as_attributes(self):
- cat = catalog.Catalog()
- cat[''] = catalog.Message('',
- "Last-Translator: Foo Bar <foo.bar@example.com>\n" +
- "Language-Team: de <de@example.com>\n" +
- "POT-Creation-Date: 2009-03-01 11:20+0200\n" +
- "PO-Revision-Date: 2009-03-09 15:47-0700\n")
- assert cat.locale is None
- mime_headers = dict(cat.mime_headers)
-
- assert cat.last_translator == 'Foo Bar <foo.bar@example.com>'
- assert mime_headers['Last-Translator'] == 'Foo Bar <foo.bar@example.com>'
-
- assert cat.language_team == 'de <de@example.com>'
- assert mime_headers['Language-Team'] == 'de <de@example.com>'
-
- dt = datetime.datetime(2009, 3, 9, 15, 47, tzinfo=FixedOffsetTimezone(-7 * 60))
- assert cat.revision_date == dt
- formatted_dt = format_datetime(dt, 'yyyy-MM-dd HH:mmZ', locale='en')
- assert mime_headers['PO-Revision-Date'] == formatted_dt
+ cat = catalog.Catalog()
+ cat.add("ZZZZZZ " + lipsum, "foo")
+ tmpl = catalog.Catalog()
+ tmpl.add(lipsum + " ZZZZZZ")
+ cat.update(tmpl)
+ assert cat[lipsum + " ZZZZZZ"].fuzzy is True
+ assert len(cat.obsolete) == 0
+
+
+def test_catalog_update_without_fuzzy_matching():
+ cat = catalog.Catalog()
+ cat.add('fo', 'Voh')
+ cat.add('bar', 'Bahr')
+ tmpl = catalog.Catalog()
+ tmpl.add('foo')
+ cat.update(tmpl, no_fuzzy_matching=True)
+ assert len(cat.obsolete) == 2
+
+
+def test_catalog_fuzzy_matching_regarding_plurals():
+ cat = catalog.Catalog()
+ cat.add(('foo', 'foh'), ('foo', 'foh'))
+ ru = copy.copy(cat)
+ ru.locale = 'ru_RU'
+ ru.update(cat)
+ assert ru['foo'].fuzzy is True
+ ru = copy.copy(cat)
+ ru.locale = 'ru_RU'
+ ru['foo'].string = ('foh', 'fohh', 'fohhh')
+ ru.update(cat)
+ assert ru['foo'].fuzzy is False
+
+
+def test_catalog_update_no_template_mutation():
+ tmpl = catalog.Catalog()
+ tmpl.add('foo')
+ cat1 = catalog.Catalog()
+ cat1.add('foo', 'Voh')
+ cat1.update(tmpl)
+ cat2 = catalog.Catalog()
+ cat2.update(tmpl)
+
+ assert cat2['foo'].string is None
+ assert cat2['foo'].fuzzy is False
+
+
+def test_catalog_update_po_updates_pot_creation_date():
+ template = catalog.Catalog()
+ localized_catalog = copy.deepcopy(template)
+ localized_catalog.locale = 'de_DE'
+ assert template.mime_headers != localized_catalog.mime_headers
+ assert template.creation_date == localized_catalog.creation_date
+ template.creation_date = datetime.datetime.now() - \
+ datetime.timedelta(minutes=5)
+ localized_catalog.update(template)
+ assert template.creation_date == localized_catalog.creation_date
+
+
+def test_catalog_update_po_ignores_pot_creation_date():
+ template = catalog.Catalog()
+ localized_catalog = copy.deepcopy(template)
+ localized_catalog.locale = 'de_DE'
+ assert template.mime_headers != localized_catalog.mime_headers
+ assert template.creation_date == localized_catalog.creation_date
+ template.creation_date = datetime.datetime.now() - \
+ datetime.timedelta(minutes=5)
+ localized_catalog.update(template, update_creation_date=False)
+ assert template.creation_date != localized_catalog.creation_date
+
+
+def test_catalog_update_po_keeps_po_revision_date():
+ template = catalog.Catalog()
+ localized_catalog = copy.deepcopy(template)
+ localized_catalog.locale = 'de_DE'
+ fake_rev_date = datetime.datetime.now() - datetime.timedelta(days=5)
+ localized_catalog.revision_date = fake_rev_date
+ assert template.mime_headers != localized_catalog.mime_headers
+ assert template.creation_date == localized_catalog.creation_date
+ template.creation_date = datetime.datetime.now() - \
+ datetime.timedelta(minutes=5)
+ localized_catalog.update(template)
+ assert localized_catalog.revision_date == fake_rev_date
+
+
+def test_catalog_stores_datetime_correctly():
+ localized = catalog.Catalog()
+ localized.locale = 'de_DE'
+ localized[''] = catalog.Message('',
+ "POT-Creation-Date: 2009-03-09 15:47-0700\n" +
+ "PO-Revision-Date: 2009-03-09 15:47-0700\n")
+ for key, value in localized.mime_headers:
+ if key in ('POT-Creation-Date', 'PO-Revision-Date'):
+ assert value == '2009-03-09 15:47-0700'
+
+
+def test_catalog_mime_headers_contain_same_information_as_attributes():
+ cat = catalog.Catalog()
+ cat[''] = catalog.Message('',
+ "Last-Translator: Foo Bar <foo.bar@example.com>\n" +
+ "Language-Team: de <de@example.com>\n" +
+ "POT-Creation-Date: 2009-03-01 11:20+0200\n" +
+ "PO-Revision-Date: 2009-03-09 15:47-0700\n")
+ assert cat.locale is None
+ mime_headers = dict(cat.mime_headers)
+
+ assert cat.last_translator == 'Foo Bar <foo.bar@example.com>'
+ assert mime_headers['Last-Translator'] == 'Foo Bar <foo.bar@example.com>'
+
+ assert cat.language_team == 'de <de@example.com>'
+ assert mime_headers['Language-Team'] == 'de <de@example.com>'
+
+ dt = datetime.datetime(2009, 3, 9, 15, 47, tzinfo=FixedOffsetTimezone(-7 * 60))
+ assert cat.revision_date == dt
+ formatted_dt = format_datetime(dt, 'yyyy-MM-dd HH:mmZ', locale='en')
+ assert mime_headers['PO-Revision-Date'] == formatted_dt
def test_message_fuzzy():
assert catalog.Message(('foo', 'bar')).pluralizable
-def test_message_python_format():
+def test_message_python_format_2():
assert not catalog.Message('foo').python_format
assert not catalog.Message(('foo', 'foo')).python_format
assert catalog.Message('foo %(name)s bar').python_format
assert catalog.Message(('foo %(name)s', 'foo %(name)s')).python_format
-def test_message_python_brace_format():
+def test_message_python_brace_format_2():
assert not catalog.Message('foo').python_brace_format
assert not catalog.Message(('foo', 'foo')).python_brace_format
assert catalog.Message('foo {name} bar').python_brace_format
# individuals. For the exact contribution history, see the revision
# history and logs, available at https://github.com/python-babel/babel/commits/master/.
-import unittest
from datetime import datetime
from io import BytesIO
from babel.messages.pofile import read_po
from babel.util import LOCALTZ
+# the last msgstr[idx] is always missing except for singular plural forms
-class CheckersTestCase(unittest.TestCase):
- # the last msgstr[idx] is always missing except for singular plural forms
- def test_1_num_plurals_checkers(self):
- for _locale in [p for p in PLURALS if PLURALS[p][0] == 1]:
- try:
- locale = Locale.parse(_locale)
- except UnknownLocaleError:
- # Just an alias? Not what we're testing here, let's continue
- continue
- date = format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale=_locale)
- plural = PLURALS[_locale][0]
- po_file = (f"""\
+def test_1_num_plurals_checkers():
+ for _locale in [p for p in PLURALS if PLURALS[p][0] == 1]:
+ try:
+ locale = Locale.parse(_locale)
+ except UnknownLocaleError:
+ # Just an alias? Not what we're testing here, let's continue
+ continue
+ date = format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale=_locale)
+ plural = PLURALS[_locale][0]
+ po_file = (f"""\
# {locale.english_name} translations for TestProject.
# Copyright (C) 2007 FooBar, Inc.
# This file is distributed under the same license as the TestProject
""").encode('utf-8')
- # This test will fail for revisions <= 406 because so far
- # catalog.num_plurals was neglected
- catalog = read_po(BytesIO(po_file), _locale)
- message = catalog['foobar']
- checkers.num_plurals(catalog, message)
-
- def test_2_num_plurals_checkers(self):
- # in this testcase we add an extra msgstr[idx], we should be
- # disregarding it
- for _locale in [p for p in PLURALS if PLURALS[p][0] == 2]:
- if _locale in ['nn', 'no']:
- _locale = 'nn_NO'
- num_plurals = PLURALS[_locale.split('_')[0]][0]
- plural_expr = PLURALS[_locale.split('_')[0]][1]
- else:
- num_plurals = PLURALS[_locale][0]
- plural_expr = PLURALS[_locale][1]
- try:
- locale = Locale(_locale)
- date = format_datetime(datetime.now(LOCALTZ),
- 'yyyy-MM-dd HH:mmZ',
- tzinfo=LOCALTZ, locale=_locale)
- except UnknownLocaleError:
- # Just an alias? Not what we're testing here, let's continue
- continue
- po_file = f"""\
+ # This test will fail for revisions <= 406 because so far
+ # catalog.num_plurals was neglected
+ catalog = read_po(BytesIO(po_file), _locale)
+ message = catalog['foobar']
+ checkers.num_plurals(catalog, message)
+
+
+def test_2_num_plurals_checkers():
+ # in this testcase we add an extra msgstr[idx], we should be
+ # disregarding it
+ for _locale in [p for p in PLURALS if PLURALS[p][0] == 2]:
+ if _locale in ['nn', 'no']:
+ _locale = 'nn_NO'
+ num_plurals = PLURALS[_locale.split('_')[0]][0]
+ plural_expr = PLURALS[_locale.split('_')[0]][1]
+ else:
+ num_plurals = PLURALS[_locale][0]
+ plural_expr = PLURALS[_locale][1]
+ try:
+ locale = Locale(_locale)
+ date = format_datetime(datetime.now(LOCALTZ),
+ 'yyyy-MM-dd HH:mmZ',
+ tzinfo=LOCALTZ, locale=_locale)
+ except UnknownLocaleError:
+ # Just an alias? Not what we're testing here, let's continue
+ continue
+ po_file = f"""\
# {locale.english_name} translations for TestProject.
# Copyright (C) 2007 FooBar, Inc.
# This file is distributed under the same license as the TestProject
msgstr[2] ""
""".encode('utf-8')
- # we should be adding the missing msgstr[0]
-
- # This test will fail for revisions <= 406 because so far
- # catalog.num_plurals was neglected
- catalog = read_po(BytesIO(po_file), _locale)
- message = catalog['foobar']
- checkers.num_plurals(catalog, message)
-
- def test_3_num_plurals_checkers(self):
- for _locale in [p for p in PLURALS if PLURALS[p][0] == 3]:
- plural = format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale=_locale)
- english_name = Locale.parse(_locale).english_name
- po_file = fr"""\
+ # we should be adding the missing msgstr[0]
+
+ # This test will fail for revisions <= 406 because so far
+ # catalog.num_plurals was neglected
+ catalog = read_po(BytesIO(po_file), _locale)
+ message = catalog['foobar']
+ checkers.num_plurals(catalog, message)
+
+
+def test_3_num_plurals_checkers():
+ for _locale in [p for p in PLURALS if PLURALS[p][0] == 3]:
+ plural = format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale=_locale)
+ english_name = Locale.parse(_locale).english_name
+ po_file = fr"""\
# {english_name} translations for TestProject.
# Copyright (C) 2007 FooBar, Inc.
# This file is distributed under the same license as the TestProject
""".encode('utf-8')
- # This test will fail for revisions <= 406 because so far
- # catalog.num_plurals was neglected
- catalog = read_po(BytesIO(po_file), _locale)
- message = catalog['foobar']
- checkers.num_plurals(catalog, message)
-
- def test_4_num_plurals_checkers(self):
- for _locale in [p for p in PLURALS if PLURALS[p][0] == 4]:
- date = format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale=_locale)
- english_name = Locale.parse(_locale).english_name
- plural = PLURALS[_locale][0]
- po_file = fr"""\
+ # This test will fail for revisions <= 406 because so far
+ # catalog.num_plurals was neglected
+ catalog = read_po(BytesIO(po_file), _locale)
+ message = catalog['foobar']
+ checkers.num_plurals(catalog, message)
+
+
+def test_4_num_plurals_checkers():
+ for _locale in [p for p in PLURALS if PLURALS[p][0] == 4]:
+ date = format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale=_locale)
+ english_name = Locale.parse(_locale).english_name
+ plural = PLURALS[_locale][0]
+ po_file = fr"""\
# {english_name} translations for TestProject.
# Copyright (C) 2007 FooBar, Inc.
# This file is distributed under the same license as the TestProject
""".encode('utf-8')
- # This test will fail for revisions <= 406 because so far
- # catalog.num_plurals was neglected
- catalog = read_po(BytesIO(po_file), _locale)
- message = catalog['foobar']
- checkers.num_plurals(catalog, message)
-
- def test_5_num_plurals_checkers(self):
- for _locale in [p for p in PLURALS if PLURALS[p][0] == 5]:
- date = format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale=_locale)
- english_name = Locale.parse(_locale).english_name
- plural = PLURALS[_locale][0]
- po_file = fr"""\
+ # This test will fail for revisions <= 406 because so far
+ # catalog.num_plurals was neglected
+ catalog = read_po(BytesIO(po_file), _locale)
+ message = catalog['foobar']
+ checkers.num_plurals(catalog, message)
+
+
+def test_5_num_plurals_checkers():
+ for _locale in [p for p in PLURALS if PLURALS[p][0] == 5]:
+ date = format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale=_locale)
+ english_name = Locale.parse(_locale).english_name
+ plural = PLURALS[_locale][0]
+ po_file = fr"""\
# {english_name} translations for TestProject.
# Copyright (C) 2007 FooBar, Inc.
# This file is distributed under the same license as the TestProject
""".encode('utf-8')
- # This test will fail for revisions <= 406 because so far
- # catalog.num_plurals was neglected
- catalog = read_po(BytesIO(po_file), _locale)
- message = catalog['foobar']
- checkers.num_plurals(catalog, message)
-
- def test_6_num_plurals_checkers(self):
- for _locale in [p for p in PLURALS if PLURALS[p][0] == 6]:
- english_name = Locale.parse(_locale).english_name
- date = format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale=_locale)
- plural = PLURALS[_locale][0]
- po_file = fr"""\
+ # This test will fail for revisions <= 406 because so far
+ # catalog.num_plurals was neglected
+ catalog = read_po(BytesIO(po_file), _locale)
+ message = catalog['foobar']
+ checkers.num_plurals(catalog, message)
+
+
+def test_6_num_plurals_checkers():
+ for _locale in [p for p in PLURALS if PLURALS[p][0] == 6]:
+ english_name = Locale.parse(_locale).english_name
+ date = format_datetime(datetime.now(LOCALTZ), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale=_locale)
+ plural = PLURALS[_locale][0]
+ po_file = fr"""\
# {english_name} translations for TestProject.
# Copyright (C) 2007 FooBar, Inc.
# This file is distributed under the same license as the TestProject
""".encode('utf-8')
- # This test will fail for revisions <= 406 because so far
- # catalog.num_plurals was neglected
- catalog = read_po(BytesIO(po_file), _locale)
- message = catalog['foobar']
- checkers.num_plurals(catalog, message)
-
-
-class TestPythonFormat:
- @pytest.mark.parametrize(('msgid', 'msgstr'), [
- ('foo %s', 'foo'),
- (('foo %s', 'bar'), ('foo', 'bar')),
- (('foo', 'bar %s'), ('foo', 'bar')),
- (('foo %s', 'bar'), ('foo')),
- (('foo %s', 'bar %d'), ('foo %s', 'bar %d', 'baz')),
- (('foo %s', 'bar %d'), ('foo %s', 'bar %d', 'baz %d', 'qux')),
- ])
- def test_python_format_invalid(self, msgid, msgstr):
- msg = Message(msgid, msgstr)
- with pytest.raises(TranslationError):
- python_format(None, msg)
-
- @pytest.mark.parametrize(('msgid', 'msgstr'), [
- ('foo', 'foo'),
- ('foo', 'foo %s'),
- ('foo %s', ''),
- (('foo %s', 'bar %d'), ('foo %s', 'bar %d')),
- (('foo %s', 'bar %d'), ('foo %s', 'bar %d', 'baz %d')),
- (('foo', 'bar %s'), ('foo')),
- (('foo', 'bar %s'), ('', '')),
- (('foo', 'bar %s'), ('foo', '')),
- (('foo %s', 'bar %d'), ('foo %s', '')),
- ])
- def test_python_format_valid(self, msgid, msgstr):
- msg = Message(msgid, msgstr)
+ # This test will fail for revisions <= 406 because so far
+ # catalog.num_plurals was neglected
+ catalog = read_po(BytesIO(po_file), _locale)
+ message = catalog['foobar']
+ checkers.num_plurals(catalog, message)
+
+
+@pytest.mark.parametrize(('msgid', 'msgstr'), [
+ ('foo %s', 'foo'),
+ (('foo %s', 'bar'), ('foo', 'bar')),
+ (('foo', 'bar %s'), ('foo', 'bar')),
+ (('foo %s', 'bar'), ('foo')),
+ (('foo %s', 'bar %d'), ('foo %s', 'bar %d', 'baz')),
+ (('foo %s', 'bar %d'), ('foo %s', 'bar %d', 'baz %d', 'qux')),
+])
+def test_python_format_invalid(msgid, msgstr):
+ msg = Message(msgid, msgstr)
+ with pytest.raises(TranslationError):
python_format(None, msg)
- @pytest.mark.parametrize(('msgid', 'msgstr', 'error'), [
- ('%s %(foo)s', '%s %(foo)s', 'format string mixes positional and named placeholders'),
- ('foo %s', 'foo', 'placeholders are incompatible'),
- ('%s', '%(foo)s', 'the format strings are of different kinds'),
- ('%s', '%s %d', 'positional format placeholders are unbalanced'),
- ('%s', '%d', "incompatible format for placeholder 1: 's' and 'd' are not compatible"),
- ('%s %s %d', '%s %s %s', "incompatible format for placeholder 3: 'd' and 's' are not compatible"),
- ('%(foo)s', '%(bar)s', "unknown named placeholder 'bar'"),
- ('%(foo)s', '%(bar)d', "unknown named placeholder 'bar'"),
- ('%(foo)s', '%(foo)d', "incompatible format for placeholder 'foo': 'd' and 's' are not compatible"),
- ])
- def test__validate_format_invalid(self, msgid, msgstr, error):
- with pytest.raises(TranslationError, match=error):
- _validate_format(msgid, msgstr)
-
- @pytest.mark.parametrize(('msgid', 'msgstr'), [
- ('foo', 'foo'),
- ('foo', 'foo %s'),
- ('%s foo', 'foo %s'),
- ('%i', '%d'),
- ('%d', '%u'),
- ('%x', '%X'),
- ('%f', '%F'),
- ('%F', '%g'),
- ('%g', '%G'),
- ('%(foo)s', 'foo'),
- ('%(foo)s', '%(foo)s %(foo)s'),
- ('%(bar)s foo %(n)d', '%(n)d foo %(bar)s'),
- ])
- def test__validate_format_valid(self, msgid, msgstr):
+
+@pytest.mark.parametrize(('msgid', 'msgstr'), [
+ ('foo', 'foo'),
+ ('foo', 'foo %s'),
+ ('foo %s', ''),
+ (('foo %s', 'bar %d'), ('foo %s', 'bar %d')),
+ (('foo %s', 'bar %d'), ('foo %s', 'bar %d', 'baz %d')),
+ (('foo', 'bar %s'), ('foo')),
+ (('foo', 'bar %s'), ('', '')),
+ (('foo', 'bar %s'), ('foo', '')),
+ (('foo %s', 'bar %d'), ('foo %s', '')),
+])
+def test_python_format_valid(msgid, msgstr):
+ msg = Message(msgid, msgstr)
+ python_format(None, msg)
+
+
+@pytest.mark.parametrize(('msgid', 'msgstr', 'error'), [
+ ('%s %(foo)s', '%s %(foo)s', 'format string mixes positional and named placeholders'),
+ ('foo %s', 'foo', 'placeholders are incompatible'),
+ ('%s', '%(foo)s', 'the format strings are of different kinds'),
+ ('%s', '%s %d', 'positional format placeholders are unbalanced'),
+ ('%s', '%d', "incompatible format for placeholder 1: 's' and 'd' are not compatible"),
+ ('%s %s %d', '%s %s %s', "incompatible format for placeholder 3: 'd' and 's' are not compatible"),
+ ('%(foo)s', '%(bar)s', "unknown named placeholder 'bar'"),
+ ('%(foo)s', '%(bar)d', "unknown named placeholder 'bar'"),
+ ('%(foo)s', '%(foo)d', "incompatible format for placeholder 'foo': 'd' and 's' are not compatible"),
+])
+def test__validate_format_invalid(msgid, msgstr, error):
+ with pytest.raises(TranslationError, match=error):
_validate_format(msgid, msgstr)
+
+
+@pytest.mark.parametrize(('msgid', 'msgstr'), [
+ ('foo', 'foo'),
+ ('foo', 'foo %s'),
+ ('%s foo', 'foo %s'),
+ ('%i', '%d'),
+ ('%d', '%u'),
+ ('%x', '%X'),
+ ('%f', '%F'),
+ ('%F', '%g'),
+ ('%g', '%G'),
+ ('%(foo)s', 'foo'),
+ ('%(foo)s', '%(foo)s %(foo)s'),
+ ('%(bar)s foo %(n)d', '%(n)d foo %(bar)s'),
+])
+def test__validate_format_valid(msgid, msgstr):
+ _validate_format(msgid, msgstr)
# individuals. For the exact contribution history, see the revision
# history and logs, available at https://github.com/python-babel/babel/commits/master/.
-import codecs
import sys
-import unittest
from io import BytesIO, StringIO
import pytest
from babel.messages import extract
-class ExtractPythonTestCase(unittest.TestCase):
-
- def test_nested_calls(self):
- buf = BytesIO(b"""\
+def test_invalid_filter():
+ buf = BytesIO(b"""\
msg1 = _(i18n_arg.replace(r'\"', '"'))
msg2 = ungettext(i18n_arg.replace(r'\"', '"'), multi_arg.replace(r'\"', '"'), 2)
msg3 = ungettext("Babel", multi_arg.replace(r'\"', '"'), 2)
msg7 = _(hello.there)
msg8 = gettext('Rabbit')
msg9 = dgettext('wiki', model.addPage())
-msg10 = dngettext(getDomain(), 'Page', 'Pages', 3)
-msg11 = ngettext(
- "bunny",
- "bunnies",
- len(bunnies)
-)
-""")
- messages = list(extract.extract_python(buf,
- extract.DEFAULT_KEYWORDS.keys(),
- [], {}))
- assert messages == [
- (1, '_', None, []),
- (2, 'ungettext', (None, None, None), []),
- (3, 'ungettext', ('Babel', None, None), []),
- (4, 'ungettext', (None, 'Babels', None), []),
- (5, 'ungettext', ('bunny', 'bunnies', None), []),
- (6, 'ungettext', (None, 'bunnies', None), []),
- (7, '_', None, []),
- (8, 'gettext', 'Rabbit', []),
- (9, 'dgettext', ('wiki', None), []),
- (10, 'dngettext', (None, 'Page', 'Pages', None), []),
- (12, 'ngettext', ('bunny', 'bunnies', None), []),
- ]
-
- def test_extract_default_encoding_ascii(self):
- buf = BytesIO(b'_("a")')
- messages = list(extract.extract_python(
- buf, list(extract.DEFAULT_KEYWORDS), [], {},
- ))
- # Should work great in both py2 and py3
- assert messages == [(1, '_', 'a', [])]
-
- def test_extract_default_encoding_utf8(self):
- buf = BytesIO('_("☃")'.encode('UTF-8'))
- messages = list(extract.extract_python(
- buf, list(extract.DEFAULT_KEYWORDS), [], {},
- ))
- assert messages == [(1, '_', '☃', [])]
-
- def test_nested_comments(self):
- buf = BytesIO(b"""\
-msg = ngettext('pylon', # TRANSLATORS: shouldn't be
- 'pylons', # TRANSLATORS: seeing this
- count)
-""")
- messages = list(extract.extract_python(buf, ('ngettext',),
- ['TRANSLATORS:'], {}))
- assert messages == [(1, 'ngettext', ('pylon', 'pylons', None), [])]
-
- def test_comments_with_calls_that_spawn_multiple_lines(self):
- buf = BytesIO(b"""\
-# NOTE: This Comment SHOULD Be Extracted
-add_notice(req, ngettext("Catalog deleted.",
- "Catalogs deleted.", len(selected)))
-
-# NOTE: This Comment SHOULD Be Extracted
-add_notice(req, _("Locale deleted."))
-
-
-# NOTE: This Comment SHOULD Be Extracted
-add_notice(req, ngettext("Foo deleted.", "Foos deleted.", len(selected)))
-
-# NOTE: This Comment SHOULD Be Extracted
-# NOTE: And This One Too
-add_notice(req, ngettext("Bar deleted.",
- "Bars deleted.", len(selected)))
-""")
- messages = list(extract.extract_python(buf, ('ngettext', '_'), ['NOTE:'],
-
- {'strip_comment_tags': False}))
- assert messages[0] == (2, 'ngettext', ('Catalog deleted.', 'Catalogs deleted.', None), ['NOTE: This Comment SHOULD Be Extracted'])
- assert messages[1] == (6, '_', 'Locale deleted.', ['NOTE: This Comment SHOULD Be Extracted'])
- assert messages[2] == (10, 'ngettext', ('Foo deleted.', 'Foos deleted.', None), ['NOTE: This Comment SHOULD Be Extracted'])
- assert messages[3] == (14, 'ngettext', ('Bar deleted.', 'Bars deleted.', None), ['NOTE: This Comment SHOULD Be Extracted', 'NOTE: And This One Too'])
-
- def test_declarations(self):
- buf = BytesIO(b"""\
-class gettext(object):
- pass
-def render_body(context,x,y=_('Page arg 1'),z=_('Page arg 2'),**pageargs):
- pass
-def ngettext(y='arg 1',z='arg 2',**pageargs):
- pass
-class Meta:
- verbose_name = _('log entry')
-""")
- messages = list(extract.extract_python(buf,
- extract.DEFAULT_KEYWORDS.keys(),
- [], {}))
- assert messages == [
- (3, '_', 'Page arg 1', []),
- (3, '_', 'Page arg 2', []),
- (8, '_', 'log entry', []),
- ]
-
- def test_multiline(self):
- buf = BytesIO(b"""\
-msg1 = ngettext('pylon',
- 'pylons', count)
-msg2 = ngettext('elvis',
- 'elvises',
- count)
-""")
- messages = list(extract.extract_python(buf, ('ngettext',), [], {}))
- assert messages == [
- (1, 'ngettext', ('pylon', 'pylons', None), []),
- (3, 'ngettext', ('elvis', 'elvises', None), []),
- ]
-
- def test_npgettext(self):
- buf = BytesIO(b"""\
-msg1 = npgettext('Strings','pylon',
- 'pylons', count)
-msg2 = npgettext('Strings','elvis',
- 'elvises',
- count)
-""")
- messages = list(extract.extract_python(buf, ('npgettext',), [], {}))
- assert messages == [
- (1, 'npgettext', ('Strings', 'pylon', 'pylons', None), []),
- (3, 'npgettext', ('Strings', 'elvis', 'elvises', None), []),
- ]
- buf = BytesIO(b"""\
-msg = npgettext('Strings', 'pylon', # TRANSLATORS: shouldn't be
- 'pylons', # TRANSLATORS: seeing this
- count)
-""")
- messages = list(extract.extract_python(buf, ('npgettext',),
- ['TRANSLATORS:'], {}))
- assert messages == [
- (1, 'npgettext', ('Strings', 'pylon', 'pylons', None), []),
- ]
-
- def test_triple_quoted_strings(self):
- buf = BytesIO(b"""\
-msg1 = _('''pylons''')
-msg2 = ngettext(r'''elvis''', \"\"\"elvises\"\"\", count)
-msg2 = ngettext(\"\"\"elvis\"\"\", 'elvises', count)
-""")
- messages = list(extract.extract_python(buf,
- extract.DEFAULT_KEYWORDS.keys(),
- [], {}))
- assert messages == [
- (1, '_', 'pylons', []),
- (2, 'ngettext', ('elvis', 'elvises', None), []),
- (3, 'ngettext', ('elvis', 'elvises', None), []),
- ]
-
- def test_multiline_strings(self):
- buf = BytesIO(b"""\
-_('''This module provides internationalization and localization
-support for your Python programs by providing an interface to the GNU
-gettext message catalog library.''')
-""")
- messages = list(extract.extract_python(buf,
- extract.DEFAULT_KEYWORDS.keys(),
- [], {}))
- assert messages == [
- (1, '_',
- 'This module provides internationalization and localization\n'
- 'support for your Python programs by providing an interface to '
- 'the GNU\ngettext message catalog library.', []),
- ]
-
- def test_concatenated_strings(self):
- buf = BytesIO(b"""\
-foobar = _('foo' 'bar')
-""")
- messages = list(extract.extract_python(buf,
- extract.DEFAULT_KEYWORDS.keys(),
- [], {}))
- assert messages[0][2] == 'foobar'
-
- def test_unicode_string_arg(self):
- buf = BytesIO(b"msg = _('Foo Bar')")
- messages = list(extract.extract_python(buf, ('_',), [], {}))
- assert messages[0][2] == 'Foo Bar'
-
- def test_comment_tag(self):
- buf = BytesIO(b"""
-# NOTE: A translation comment
-msg = _('Foo Bar')
-""")
- messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {}))
- assert messages[0][2] == 'Foo Bar'
- assert messages[0][3] == ['NOTE: A translation comment']
-
- def test_comment_tag_multiline(self):
- buf = BytesIO(b"""
-# NOTE: A translation comment
-# with a second line
-msg = _('Foo Bar')
-""")
- messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {}))
- assert messages[0][2] == 'Foo Bar'
- assert messages[0][3] == ['NOTE: A translation comment', 'with a second line']
-
- def test_translator_comments_with_previous_non_translator_comments(self):
- buf = BytesIO(b"""
-# This shouldn't be in the output
-# because it didn't start with a comment tag
-# NOTE: A translation comment
-# with a second line
-msg = _('Foo Bar')
-""")
- messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {}))
- assert messages[0][2] == 'Foo Bar'
- assert messages[0][3] == ['NOTE: A translation comment', 'with a second line']
-
- def test_comment_tags_not_on_start_of_comment(self):
- buf = BytesIO(b"""
-# This shouldn't be in the output
-# because it didn't start with a comment tag
-# do NOTE: this will not be a translation comment
-# NOTE: This one will be
-msg = _('Foo Bar')
-""")
- messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {}))
- assert messages[0][2] == 'Foo Bar'
- assert messages[0][3] == ['NOTE: This one will be']
-
- def test_multiple_comment_tags(self):
- buf = BytesIO(b"""
-# NOTE1: A translation comment for tag1
-# with a second line
-msg = _('Foo Bar1')
-
-# NOTE2: A translation comment for tag2
-msg = _('Foo Bar2')
-""")
- messages = list(extract.extract_python(buf, ('_',),
- ['NOTE1:', 'NOTE2:'], {}))
- assert messages[0][2] == 'Foo Bar1'
- assert messages[0][3] == ['NOTE1: A translation comment for tag1', 'with a second line']
- assert messages[1][2] == 'Foo Bar2'
- assert messages[1][3] == ['NOTE2: A translation comment for tag2']
-
- def test_two_succeeding_comments(self):
- buf = BytesIO(b"""
-# NOTE: one
-# NOTE: two
-msg = _('Foo Bar')
-""")
- messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {}))
- assert messages[0][2] == 'Foo Bar'
- assert messages[0][3] == ['NOTE: one', 'NOTE: two']
-
- def test_invalid_translator_comments(self):
- buf = BytesIO(b"""
-# NOTE: this shouldn't apply to any messages
-hello = 'there'
-
-msg = _('Foo Bar')
+msg10 = dngettext(domain, 'Page', 'Pages', 3)
""")
- messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {}))
- assert messages[0][2] == 'Foo Bar'
- assert messages[0][3] == []
+ messages = \
+ list(extract.extract('python', buf, extract.DEFAULT_KEYWORDS, [],
+ {}))
+ assert messages == [
+ (5, ('bunny', 'bunnies'), [], None),
+ (8, 'Rabbit', [], None),
+ (10, ('Page', 'Pages'), [], None),
+ ]
- def test_invalid_translator_comments2(self):
- buf = BytesIO(b"""
-# NOTE: Hi!
-hithere = _('Hi there!')
-# NOTE: you should not be seeing this in the .po
-rows = [[v for v in range(0,10)] for row in range(0,10)]
+def test_invalid_extract_method():
+ buf = BytesIO(b'')
+ with pytest.raises(ValueError):
+ list(extract.extract('spam', buf))
-# this (NOTE:) should not show up either
-hello = _('Hello')
-""")
- messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {}))
- assert messages[0][2] == 'Hi there!'
- assert messages[0][3] == ['NOTE: Hi!']
- assert messages[1][2] == 'Hello'
- assert messages[1][3] == []
-
- def test_invalid_translator_comments3(self):
- buf = BytesIO(b"""
-# NOTE: Hi,
-
-# there!
-hithere = _('Hi there!')
-""")
- messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {}))
- assert messages[0][2] == 'Hi there!'
- assert messages[0][3] == []
-
- def test_comment_tag_with_leading_space(self):
- buf = BytesIO(b"""
- #: A translation comment
- #: with leading spaces
-msg = _('Foo Bar')
-""")
- messages = list(extract.extract_python(buf, ('_',), [':'], {}))
- assert messages[0][2] == 'Foo Bar'
- assert messages[0][3] == [': A translation comment', ': with leading spaces']
- def test_different_signatures(self):
- buf = BytesIO(b"""
-foo = _('foo', 'bar')
-n = ngettext('hello', 'there', n=3)
-n = ngettext(n=3, 'hello', 'there')
-n = ngettext(n=3, *messages)
-n = ngettext()
-n = ngettext('foo')
-""")
- messages = list(extract.extract_python(buf, ('_', 'ngettext'), [], {}))
- assert messages[0][2] == ('foo', 'bar')
- assert messages[1][2] == ('hello', 'there', None)
- assert messages[2][2] == (None, 'hello', 'there')
- assert messages[3][2] == (None, None)
- assert messages[4][2] is None
- assert messages[5][2] == 'foo'
-
- def test_utf8_message(self):
- buf = BytesIO("""
-# NOTE: hello
-msg = _('Bonjour à tous')
-""".encode('utf-8'))
- messages = list(extract.extract_python(buf, ('_',), ['NOTE:'],
- {'encoding': 'utf-8'}))
- assert messages[0][2] == 'Bonjour à tous'
- assert messages[0][3] == ['NOTE: hello']
-
- def test_utf8_message_with_magic_comment(self):
- buf = BytesIO("""# -*- coding: utf-8 -*-
-# NOTE: hello
-msg = _('Bonjour à tous')
-""".encode('utf-8'))
- messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {}))
- assert messages[0][2] == 'Bonjour à tous'
- assert messages[0][3] == ['NOTE: hello']
-
- def test_utf8_message_with_utf8_bom(self):
- buf = BytesIO(codecs.BOM_UTF8 + """
-# NOTE: hello
-msg = _('Bonjour à tous')
-""".encode('utf-8'))
- messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {}))
- assert messages[0][2] == 'Bonjour à tous'
- assert messages[0][3] == ['NOTE: hello']
-
- def test_utf8_message_with_utf8_bom_and_magic_comment(self):
- buf = BytesIO(codecs.BOM_UTF8 + """# -*- coding: utf-8 -*-
-# NOTE: hello
-msg = _('Bonjour à tous')
-""".encode('utf-8'))
- messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {}))
- assert messages[0][2] == 'Bonjour à tous'
- assert messages[0][3] == ['NOTE: hello']
-
- def test_utf8_bom_with_latin_magic_comment_fails(self):
- buf = BytesIO(codecs.BOM_UTF8 + """# -*- coding: latin-1 -*-
-# NOTE: hello
-msg = _('Bonjour à tous')
-""".encode('utf-8'))
- with pytest.raises(SyntaxError):
- list(extract.extract_python(buf, ('_',), ['NOTE:'], {}))
-
- def test_utf8_raw_strings_match_unicode_strings(self):
- buf = BytesIO(codecs.BOM_UTF8 + """
-msg = _('Bonjour à tous')
-msgu = _('Bonjour à tous')
-""".encode('utf-8'))
- messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {}))
- assert messages[0][2] == 'Bonjour à tous'
- assert messages[0][2] == messages[1][2]
-
- def test_extract_strip_comment_tags(self):
- buf = BytesIO(b"""\
-#: This is a comment with a very simple
-#: prefix specified
-_('Servus')
-
-# NOTE: This is a multiline comment with
-# a prefix too
-_('Babatschi')""")
- messages = list(extract.extract('python', buf, comment_tags=['NOTE:', ':'],
- strip_comment_tags=True))
- assert messages[0][1] == 'Servus'
- assert messages[0][2] == ['This is a comment with a very simple', 'prefix specified']
- assert messages[1][1] == 'Babatschi'
- assert messages[1][2] == ['This is a multiline comment with', 'a prefix too']
-
- def test_nested_messages(self):
- buf = BytesIO(b"""
-# NOTE: First
-_('Hello, {name}!', name=_('Foo Bar'))
-
-# NOTE: Second
-_('Hello, {name1} and {name2}!', name1=_('Heungsub'),
- name2=_('Armin'))
-
-# NOTE: Third
-_('Hello, {0} and {1}!', _('Heungsub'),
- _('Armin'))
-""")
- messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {}))
- assert messages[0][2] == ('Hello, {name}!', None)
- assert messages[0][3] == ['NOTE: First']
- assert messages[1][2] == 'Foo Bar'
- assert messages[1][3] == []
- assert messages[2][2] == ('Hello, {name1} and {name2}!', None)
- assert messages[2][3] == ['NOTE: Second']
- assert messages[3][2] == 'Heungsub'
- assert messages[3][3] == []
- assert messages[4][2] == 'Armin'
- assert messages[4][3] == []
- assert messages[5][2] == ('Hello, {0} and {1}!', None)
- assert messages[5][3] == ['NOTE: Third']
- assert messages[6][2] == 'Heungsub'
- assert messages[6][3] == []
- assert messages[7][2] == 'Armin'
- assert messages[7][3] == []
-
-
-class ExtractTestCase(unittest.TestCase):
-
- def test_invalid_filter(self):
- buf = BytesIO(b"""\
-msg1 = _(i18n_arg.replace(r'\"', '"'))
-msg2 = ungettext(i18n_arg.replace(r'\"', '"'), multi_arg.replace(r'\"', '"'), 2)
-msg3 = ungettext("Babel", multi_arg.replace(r'\"', '"'), 2)
-msg4 = ungettext(i18n_arg.replace(r'\"', '"'), "Babels", 2)
-msg5 = ungettext('bunny', 'bunnies', random.randint(1, 2))
-msg6 = ungettext(arg0, 'bunnies', random.randint(1, 2))
-msg7 = _(hello.there)
-msg8 = gettext('Rabbit')
-msg9 = dgettext('wiki', model.addPage())
-msg10 = dngettext(domain, 'Page', 'Pages', 3)
-""")
- messages = \
- list(extract.extract('python', buf, extract.DEFAULT_KEYWORDS, [],
- {}))
- assert messages == [
- (5, ('bunny', 'bunnies'), [], None),
- (8, 'Rabbit', [], None),
- (10, ('Page', 'Pages'), [], None),
- ]
-
- def test_invalid_extract_method(self):
- buf = BytesIO(b'')
- with pytest.raises(ValueError):
- list(extract.extract('spam', buf))
-
- def test_different_signatures(self):
- buf = BytesIO(b"""
+def test_different_signatures():
+ buf = BytesIO(b"""
foo = _('foo', 'bar')
n = ngettext('hello', 'there', n=3)
n = ngettext(n=3, 'hello', 'there')
n = ngettext()
n = ngettext('foo')
""")
- messages = \
- list(extract.extract('python', buf, extract.DEFAULT_KEYWORDS, [],
- {}))
- assert len(messages) == 2
- assert messages[0][1] == 'foo'
- assert messages[1][1] == ('hello', 'there')
-
- def test_empty_string_msgid(self):
- buf = BytesIO(b"""\
+ messages = \
+ list(extract.extract('python', buf, extract.DEFAULT_KEYWORDS, [],
+ {}))
+ assert len(messages) == 2
+ assert messages[0][1] == 'foo'
+ assert messages[1][1] == ('hello', 'there')
+
+
+def test_empty_string_msgid():
+ buf = BytesIO(b"""\
msg = _('')
""")
- stderr = sys.stderr
- sys.stderr = StringIO()
- try:
- messages = \
- list(extract.extract('python', buf, extract.DEFAULT_KEYWORDS,
- [], {}))
- assert messages == []
- assert 'warning: Empty msgid.' in sys.stderr.getvalue()
- finally:
- sys.stderr = stderr
-
- def test_warn_if_empty_string_msgid_found_in_context_aware_extraction_method(self):
- buf = BytesIO(b"\nmsg = pgettext('ctxt', '')\n")
- stderr = sys.stderr
- sys.stderr = StringIO()
- try:
- messages = extract.extract('python', buf)
- assert list(messages) == []
- assert 'warning: Empty msgid.' in sys.stderr.getvalue()
- finally:
- sys.stderr = stderr
-
- def test_extract_allows_callable(self):
- def arbitrary_extractor(fileobj, keywords, comment_tags, options):
- return [(1, None, (), ())]
- for x in extract.extract(arbitrary_extractor, BytesIO(b"")):
- assert x[0] == 1
-
- def test_future(self):
- buf = BytesIO(br"""
+ stderr = sys.stderr
+ sys.stderr = StringIO()
+ try:
+ messages = \
+ list(extract.extract('python', buf, extract.DEFAULT_KEYWORDS,
+ [], {}))
+ assert messages == []
+ assert 'warning: Empty msgid.' in sys.stderr.getvalue()
+ finally:
+ sys.stderr = stderr
+
+
+def test_warn_if_empty_string_msgid_found_in_context_aware_extraction_method():
+ buf = BytesIO(b"\nmsg = pgettext('ctxt', '')\n")
+ stderr = sys.stderr
+ sys.stderr = StringIO()
+ try:
+ messages = extract.extract('python', buf)
+ assert list(messages) == []
+ assert 'warning: Empty msgid.' in sys.stderr.getvalue()
+ finally:
+ sys.stderr = stderr
+
+
+def test_extract_allows_callable():
+ def arbitrary_extractor(fileobj, keywords, comment_tags, options):
+ return [(1, None, (), ())]
+ for x in extract.extract(arbitrary_extractor, BytesIO(b"")):
+ assert x[0] == 1
+
+
+def test_future():
+ buf = BytesIO(br"""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
nbsp = _('\xa0')
""")
- messages = list(extract.extract('python', buf,
- extract.DEFAULT_KEYWORDS, [], {}))
- assert messages[0][1] == '\xa0'
+ messages = list(extract.extract('python', buf,
+ extract.DEFAULT_KEYWORDS, [], {}))
+ assert messages[0][1] == '\xa0'
+
- def test_f_strings(self):
- buf = BytesIO(br"""
+def test_f_strings():
+ buf = BytesIO(br"""
t1 = _('foobar')
t2 = _(f'spameggs' f'feast') # should be extracted; constant parts only
t2 = _(f'spameggs' 'kerroshampurilainen') # should be extracted (mixing f with no f)
t3 = _(f'''whoa! a ''' # should be extracted (continues on following lines)
f'flying shark'
- '... hello'
+'... hello'
)
t4 = _(f'spameggs {t1}') # should not be extracted
""")
- messages = list(extract.extract('python', buf, extract.DEFAULT_KEYWORDS, [], {}))
- assert len(messages) == 4
- assert messages[0][1] == 'foobar'
- assert messages[1][1] == 'spameggsfeast'
- assert messages[2][1] == 'spameggskerroshampurilainen'
- assert messages[3][1] == 'whoa! a flying shark... hello'
-
- def test_f_strings_non_utf8(self):
- buf = BytesIO(b"""
+ messages = list(extract.extract('python', buf, extract.DEFAULT_KEYWORDS, [], {}))
+ assert len(messages) == 4
+ assert messages[0][1] == 'foobar'
+ assert messages[1][1] == 'spameggsfeast'
+ assert messages[2][1] == 'spameggskerroshampurilainen'
+ assert messages[3][1] == 'whoa! a flying shark... hello'
+
+
+def test_f_strings_non_utf8():
+ buf = BytesIO(b"""
# -- coding: latin-1 --
t2 = _(f'\xe5\xe4\xf6' f'\xc5\xc4\xd6')
""")
- messages = list(extract.extract('python', buf, extract.DEFAULT_KEYWORDS, [], {}))
- assert len(messages) == 1
- assert messages[0][1] == 'åäöÅÄÖ'
+ messages = list(extract.extract('python', buf, extract.DEFAULT_KEYWORDS, [], {}))
+ assert len(messages) == 1
+ assert messages[0][1] == 'åäöÅÄÖ'
def test_issue_1195():
--- /dev/null
+#
+# Copyright (C) 2007-2011 Edgewall Software, 2013-2025 the Babel team
+# All rights reserved.
+#
+# This software is licensed as described in the file LICENSE, which
+# you should have received as part of this distribution. The terms
+# are also available at https://github.com/python-babel/babel/blob/master/LICENSE.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at https://github.com/python-babel/babel/commits/master/.
+
+import codecs
+from io import BytesIO
+
+import pytest
+
+from babel.messages import extract
+
+
+def test_nested_calls():
+ buf = BytesIO(b"""\
+msg1 = _(i18n_arg.replace(r'\"', '"'))
+msg2 = ungettext(i18n_arg.replace(r'\"', '"'), multi_arg.replace(r'\"', '"'), 2)
+msg3 = ungettext("Babel", multi_arg.replace(r'\"', '"'), 2)
+msg4 = ungettext(i18n_arg.replace(r'\"', '"'), "Babels", 2)
+msg5 = ungettext('bunny', 'bunnies', random.randint(1, 2))
+msg6 = ungettext(arg0, 'bunnies', random.randint(1, 2))
+msg7 = _(hello.there)
+msg8 = gettext('Rabbit')
+msg9 = dgettext('wiki', model.addPage())
+msg10 = dngettext(getDomain(), 'Page', 'Pages', 3)
+msg11 = ngettext(
+"bunny",
+"bunnies",
+len(bunnies)
+)
+""")
+ messages = list(extract.extract_python(buf,
+ extract.DEFAULT_KEYWORDS.keys(),
+ [], {}))
+ assert messages == [
+ (1, '_', None, []),
+ (2, 'ungettext', (None, None, None), []),
+ (3, 'ungettext', ('Babel', None, None), []),
+ (4, 'ungettext', (None, 'Babels', None), []),
+ (5, 'ungettext', ('bunny', 'bunnies', None), []),
+ (6, 'ungettext', (None, 'bunnies', None), []),
+ (7, '_', None, []),
+ (8, 'gettext', 'Rabbit', []),
+ (9, 'dgettext', ('wiki', None), []),
+ (10, 'dngettext', (None, 'Page', 'Pages', None), []),
+ (12, 'ngettext', ('bunny', 'bunnies', None), []),
+ ]
+
+
+def test_extract_default_encoding_ascii():
+ buf = BytesIO(b'_("a")')
+ messages = list(extract.extract_python(
+ buf, list(extract.DEFAULT_KEYWORDS), [], {},
+ ))
+ # Should work great in both py2 and py3
+ assert messages == [(1, '_', 'a', [])]
+
+
+def test_extract_default_encoding_utf8():
+ buf = BytesIO('_("☃")'.encode('UTF-8'))
+ messages = list(extract.extract_python(
+ buf, list(extract.DEFAULT_KEYWORDS), [], {},
+ ))
+ assert messages == [(1, '_', '☃', [])]
+
+
+def test_nested_comments():
+ buf = BytesIO(b"""\
+msg = ngettext('pylon', # TRANSLATORS: shouldn't be
+ 'pylons', # TRANSLATORS: seeing this
+ count)
+""")
+ messages = list(extract.extract_python(buf, ('ngettext',),
+ ['TRANSLATORS:'], {}))
+ assert messages == [(1, 'ngettext', ('pylon', 'pylons', None), [])]
+
+
+def test_comments_with_calls_that_spawn_multiple_lines():
+ buf = BytesIO(b"""\
+# NOTE: This Comment SHOULD Be Extracted
+add_notice(req, ngettext("Catalog deleted.",
+ "Catalogs deleted.", len(selected)))
+
+# NOTE: This Comment SHOULD Be Extracted
+add_notice(req, _("Locale deleted."))
+
+
+# NOTE: This Comment SHOULD Be Extracted
+add_notice(req, ngettext("Foo deleted.", "Foos deleted.", len(selected)))
+
+# NOTE: This Comment SHOULD Be Extracted
+# NOTE: And This One Too
+add_notice(req, ngettext("Bar deleted.",
+ "Bars deleted.", len(selected)))
+""")
+ messages = list(extract.extract_python(buf, ('ngettext', '_'), ['NOTE:'],
+
+ {'strip_comment_tags': False}))
+ assert messages[0] == (2, 'ngettext', ('Catalog deleted.', 'Catalogs deleted.', None), ['NOTE: This Comment SHOULD Be Extracted'])
+ assert messages[1] == (6, '_', 'Locale deleted.', ['NOTE: This Comment SHOULD Be Extracted'])
+ assert messages[2] == (10, 'ngettext', ('Foo deleted.', 'Foos deleted.', None), ['NOTE: This Comment SHOULD Be Extracted'])
+ assert messages[3] == (14, 'ngettext', ('Bar deleted.', 'Bars deleted.', None), ['NOTE: This Comment SHOULD Be Extracted', 'NOTE: And This One Too'])
+
+
+def test_declarations():
+ buf = BytesIO(b"""\
+class gettext(object):
+pass
+def render_body(context,x,y=_('Page arg 1'),z=_('Page arg 2'),**pageargs):
+pass
+def ngettext(y='arg 1',z='arg 2',**pageargs):
+pass
+class Meta:
+verbose_name = _('log entry')
+""")
+ messages = list(extract.extract_python(buf,
+ extract.DEFAULT_KEYWORDS.keys(),
+ [], {}))
+ assert messages == [
+ (3, '_', 'Page arg 1', []),
+ (3, '_', 'Page arg 2', []),
+ (8, '_', 'log entry', []),
+ ]
+
+
+def test_multiline():
+ buf = BytesIO(b"""\
+msg1 = ngettext('pylon',
+ 'pylons', count)
+msg2 = ngettext('elvis',
+ 'elvises',
+ count)
+""")
+ messages = list(extract.extract_python(buf, ('ngettext',), [], {}))
+ assert messages == [
+ (1, 'ngettext', ('pylon', 'pylons', None), []),
+ (3, 'ngettext', ('elvis', 'elvises', None), []),
+ ]
+
+
+def test_npgettext():
+ buf = BytesIO(b"""\
+msg1 = npgettext('Strings','pylon',
+ 'pylons', count)
+msg2 = npgettext('Strings','elvis',
+ 'elvises',
+ count)
+""")
+ messages = list(extract.extract_python(buf, ('npgettext',), [], {}))
+ assert messages == [
+ (1, 'npgettext', ('Strings', 'pylon', 'pylons', None), []),
+ (3, 'npgettext', ('Strings', 'elvis', 'elvises', None), []),
+ ]
+ buf = BytesIO(b"""\
+msg = npgettext('Strings', 'pylon', # TRANSLATORS: shouldn't be
+ 'pylons', # TRANSLATORS: seeing this
+ count)
+""")
+ messages = list(extract.extract_python(buf, ('npgettext',),
+ ['TRANSLATORS:'], {}))
+ assert messages == [
+ (1, 'npgettext', ('Strings', 'pylon', 'pylons', None), []),
+ ]
+
+
+def test_triple_quoted_strings():
+ buf = BytesIO(b"""\
+msg1 = _('''pylons''')
+msg2 = ngettext(r'''elvis''', \"\"\"elvises\"\"\", count)
+msg2 = ngettext(\"\"\"elvis\"\"\", 'elvises', count)
+""")
+ messages = list(extract.extract_python(buf,
+ extract.DEFAULT_KEYWORDS.keys(),
+ [], {}))
+ assert messages == [
+ (1, '_', 'pylons', []),
+ (2, 'ngettext', ('elvis', 'elvises', None), []),
+ (3, 'ngettext', ('elvis', 'elvises', None), []),
+ ]
+
+
+def test_multiline_strings():
+ buf = BytesIO(b"""\
+_('''This module provides internationalization and localization
+support for your Python programs by providing an interface to the GNU
+gettext message catalog library.''')
+""")
+ messages = list(extract.extract_python(buf,
+ extract.DEFAULT_KEYWORDS.keys(),
+ [], {}))
+ assert messages == [
+ (1, '_',
+ 'This module provides internationalization and localization\n'
+ 'support for your Python programs by providing an interface to '
+ 'the GNU\ngettext message catalog library.', []),
+ ]
+
+
+def test_concatenated_strings():
+ buf = BytesIO(b"""\
+foobar = _('foo' 'bar')
+""")
+ messages = list(extract.extract_python(buf,
+ extract.DEFAULT_KEYWORDS.keys(),
+ [], {}))
+ assert messages[0][2] == 'foobar'
+
+
+def test_unicode_string_arg():
+ buf = BytesIO(b"msg = _('Foo Bar')")
+ messages = list(extract.extract_python(buf, ('_',), [], {}))
+ assert messages[0][2] == 'Foo Bar'
+
+
+def test_comment_tag():
+ buf = BytesIO(b"""
+# NOTE: A translation comment
+msg = _('Foo Bar')
+""")
+ messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {}))
+ assert messages[0][2] == 'Foo Bar'
+ assert messages[0][3] == ['NOTE: A translation comment']
+
+
+def test_comment_tag_multiline():
+ buf = BytesIO(b"""
+# NOTE: A translation comment
+# with a second line
+msg = _('Foo Bar')
+""")
+ messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {}))
+ assert messages[0][2] == 'Foo Bar'
+ assert messages[0][3] == ['NOTE: A translation comment', 'with a second line']
+
+
+def test_translator_comments_with_previous_non_translator_comments():
+ buf = BytesIO(b"""
+# This shouldn't be in the output
+# because it didn't start with a comment tag
+# NOTE: A translation comment
+# with a second line
+msg = _('Foo Bar')
+""")
+ messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {}))
+ assert messages[0][2] == 'Foo Bar'
+ assert messages[0][3] == ['NOTE: A translation comment', 'with a second line']
+
+
+def test_comment_tags_not_on_start_of_comment():
+ buf = BytesIO(b"""
+# This shouldn't be in the output
+# because it didn't start with a comment tag
+# do NOTE: this will not be a translation comment
+# NOTE: This one will be
+msg = _('Foo Bar')
+""")
+ messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {}))
+ assert messages[0][2] == 'Foo Bar'
+ assert messages[0][3] == ['NOTE: This one will be']
+
+
+def test_multiple_comment_tags():
+ buf = BytesIO(b"""
+# NOTE1: A translation comment for tag1
+# with a second line
+msg = _('Foo Bar1')
+
+# NOTE2: A translation comment for tag2
+msg = _('Foo Bar2')
+""")
+ messages = list(extract.extract_python(buf, ('_',),
+ ['NOTE1:', 'NOTE2:'], {}))
+ assert messages[0][2] == 'Foo Bar1'
+ assert messages[0][3] == ['NOTE1: A translation comment for tag1', 'with a second line']
+ assert messages[1][2] == 'Foo Bar2'
+ assert messages[1][3] == ['NOTE2: A translation comment for tag2']
+
+
+def test_two_succeeding_comments():
+ buf = BytesIO(b"""
+# NOTE: one
+# NOTE: two
+msg = _('Foo Bar')
+""")
+ messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {}))
+ assert messages[0][2] == 'Foo Bar'
+ assert messages[0][3] == ['NOTE: one', 'NOTE: two']
+
+
+def test_invalid_translator_comments():
+ buf = BytesIO(b"""
+# NOTE: this shouldn't apply to any messages
+hello = 'there'
+
+msg = _('Foo Bar')
+""")
+ messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {}))
+ assert messages[0][2] == 'Foo Bar'
+ assert messages[0][3] == []
+
+
+def test_invalid_translator_comments2():
+ buf = BytesIO(b"""
+# NOTE: Hi!
+hithere = _('Hi there!')
+
+# NOTE: you should not be seeing this in the .po
+rows = [[v for v in range(0,10)] for row in range(0,10)]
+
+# this (NOTE:) should not show up either
+hello = _('Hello')
+""")
+ messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {}))
+ assert messages[0][2] == 'Hi there!'
+ assert messages[0][3] == ['NOTE: Hi!']
+ assert messages[1][2] == 'Hello'
+ assert messages[1][3] == []
+
+
+def test_invalid_translator_comments3():
+ buf = BytesIO(b"""
+# NOTE: Hi,
+
+# there!
+hithere = _('Hi there!')
+""")
+ messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {}))
+ assert messages[0][2] == 'Hi there!'
+ assert messages[0][3] == []
+
+
+def test_comment_tag_with_leading_space():
+ buf = BytesIO(b"""
+#: A translation comment
+#: with leading spaces
+msg = _('Foo Bar')
+""")
+ messages = list(extract.extract_python(buf, ('_',), [':'], {}))
+ assert messages[0][2] == 'Foo Bar'
+ assert messages[0][3] == [': A translation comment', ': with leading spaces']
+
+
+def test_different_signatures():
+ buf = BytesIO(b"""
+foo = _('foo', 'bar')
+n = ngettext('hello', 'there', n=3)
+n = ngettext(n=3, 'hello', 'there')
+n = ngettext(n=3, *messages)
+n = ngettext()
+n = ngettext('foo')
+""")
+ messages = list(extract.extract_python(buf, ('_', 'ngettext'), [], {}))
+ assert messages[0][2] == ('foo', 'bar')
+ assert messages[1][2] == ('hello', 'there', None)
+ assert messages[2][2] == (None, 'hello', 'there')
+ assert messages[3][2] == (None, None)
+ assert messages[4][2] is None
+ assert messages[5][2] == 'foo'
+
+
+def test_utf8_message():
+ buf = BytesIO("""
+# NOTE: hello
+msg = _('Bonjour à tous')
+""".encode('utf-8'))
+ messages = list(extract.extract_python(buf, ('_',), ['NOTE:'],
+ {'encoding': 'utf-8'}))
+ assert messages[0][2] == 'Bonjour à tous'
+ assert messages[0][3] == ['NOTE: hello']
+
+
+def test_utf8_message_with_magic_comment():
+ buf = BytesIO("""# -*- coding: utf-8 -*-
+# NOTE: hello
+msg = _('Bonjour à tous')
+""".encode('utf-8'))
+ messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {}))
+ assert messages[0][2] == 'Bonjour à tous'
+ assert messages[0][3] == ['NOTE: hello']
+
+
+def test_utf8_message_with_utf8_bom():
+ buf = BytesIO(codecs.BOM_UTF8 + """
+# NOTE: hello
+msg = _('Bonjour à tous')
+""".encode('utf-8'))
+ messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {}))
+ assert messages[0][2] == 'Bonjour à tous'
+ assert messages[0][3] == ['NOTE: hello']
+
+
+def test_utf8_message_with_utf8_bom_and_magic_comment():
+ buf = BytesIO(codecs.BOM_UTF8 + """# -*- coding: utf-8 -*-
+# NOTE: hello
+msg = _('Bonjour à tous')
+""".encode('utf-8'))
+ messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {}))
+ assert messages[0][2] == 'Bonjour à tous'
+ assert messages[0][3] == ['NOTE: hello']
+
+
+def test_utf8_bom_with_latin_magic_comment_fails():
+ buf = BytesIO(codecs.BOM_UTF8 + """# -*- coding: latin-1 -*-
+# NOTE: hello
+msg = _('Bonjour à tous')
+""".encode('utf-8'))
+ with pytest.raises(SyntaxError):
+ list(extract.extract_python(buf, ('_',), ['NOTE:'], {}))
+
+
+def test_utf8_raw_strings_match_unicode_strings():
+ buf = BytesIO(codecs.BOM_UTF8 + """
+msg = _('Bonjour à tous')
+msgu = _('Bonjour à tous')
+""".encode('utf-8'))
+ messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {}))
+ assert messages[0][2] == 'Bonjour à tous'
+ assert messages[0][2] == messages[1][2]
+
+
+def test_extract_strip_comment_tags():
+ buf = BytesIO(b"""\
+#: This is a comment with a very simple
+#: prefix specified
+_('Servus')
+
+# NOTE: This is a multiline comment with
+# a prefix too
+_('Babatschi')""")
+ messages = list(extract.extract('python', buf, comment_tags=['NOTE:', ':'],
+ strip_comment_tags=True))
+ assert messages[0][1] == 'Servus'
+ assert messages[0][2] == ['This is a comment with a very simple', 'prefix specified']
+ assert messages[1][1] == 'Babatschi'
+ assert messages[1][2] == ['This is a multiline comment with', 'a prefix too']
+
+
+def test_nested_messages():
+ buf = BytesIO(b"""
+# NOTE: First
+_('Hello, {name}!', name=_('Foo Bar'))
+
+# NOTE: Second
+_('Hello, {name1} and {name2}!', name1=_('Heungsub'),
+name2=_('Armin'))
+
+# NOTE: Third
+_('Hello, {0} and {1}!', _('Heungsub'),
+_('Armin'))
+""")
+ messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {}))
+ assert messages[0][2] == ('Hello, {name}!', None)
+ assert messages[0][3] == ['NOTE: First']
+ assert messages[1][2] == 'Foo Bar'
+ assert messages[1][3] == []
+ assert messages[2][2] == ('Hello, {name1} and {name2}!', None)
+ assert messages[2][3] == ['NOTE: Second']
+ assert messages[3][2] == 'Heungsub'
+ assert messages[3][3] == []
+ assert messages[4][2] == 'Armin'
+ assert messages[4][3] == []
+ assert messages[5][2] == ('Hello, {0} and {1}!', None)
+ assert messages[5][3] == ['NOTE: Third']
+ assert messages[6][2] == 'Heungsub'
+ assert messages[6][3] == []
+ assert messages[7][2] == 'Armin'
+ assert messages[7][3] == []
+++ /dev/null
-#
-# Copyright (C) 2007-2011 Edgewall Software, 2013-2025 the Babel team
-# All rights reserved.
-#
-# This software is licensed as described in the file LICENSE, which
-# you should have received as part of this distribution. The terms
-# are also available at https://github.com/python-babel/babel/blob/master/LICENSE.
-#
-# This software consists of voluntary contributions made by many
-# individuals. For the exact contribution history, see the revision
-# history and logs, available at https://github.com/python-babel/babel/commits/master/.
-from __future__ import annotations
-
-import logging
-import os
-import re
-import shlex
-import shutil
-import sys
-import time
-import unittest
-from datetime import datetime, timedelta
-from functools import partial
-from io import BytesIO, StringIO
-
-import pytest
-from freezegun import freeze_time
-
-from babel import __version__ as VERSION
-from babel.dates import format_datetime
-from babel.messages import Catalog, extract, frontend
-from babel.messages.frontend import (
- BaseError,
- CommandLineInterface,
- ExtractMessages,
- OptionError,
- UpdateCatalog,
-)
-from babel.messages.pofile import read_po, write_po
-from babel.util import LOCALTZ
-from tests.messages.consts import (
- TEST_PROJECT_DISTRIBUTION_DATA,
- data_dir,
- i18n_dir,
- pot_file,
- project_dir,
- this_dir,
-)
-from tests.messages.utils import CUSTOM_EXTRACTOR_COOKIE
-
-
-def _po_file(locale):
- return os.path.join(i18n_dir, locale, 'LC_MESSAGES', 'messages.po')
-
-
-class Distribution: # subset of distutils.dist.Distribution
- def __init__(self, attrs: dict) -> None:
- self.attrs = attrs
-
- def get_name(self) -> str:
- return self.attrs['name']
-
- def get_version(self) -> str:
- return self.attrs['version']
-
- @property
- def packages(self) -> list[str]:
- return self.attrs['packages']
-
-
-class CompileCatalogTestCase(unittest.TestCase):
-
- def setUp(self):
- self.olddir = os.getcwd()
- os.chdir(data_dir)
-
- self.dist = Distribution(TEST_PROJECT_DISTRIBUTION_DATA)
- self.cmd = frontend.CompileCatalog(self.dist)
- self.cmd.initialize_options()
-
- def tearDown(self):
- os.chdir(self.olddir)
-
- def test_no_directory_or_output_file_specified(self):
- self.cmd.locale = 'en_US'
- self.cmd.input_file = 'dummy'
- with pytest.raises(OptionError):
- self.cmd.finalize_options()
-
- def test_no_directory_or_input_file_specified(self):
- self.cmd.locale = 'en_US'
- self.cmd.output_file = 'dummy'
- with pytest.raises(OptionError):
- self.cmd.finalize_options()
-
-
-class ExtractMessagesTestCase(unittest.TestCase):
-
- def setUp(self):
- self.olddir = os.getcwd()
- os.chdir(data_dir)
-
- self.dist = Distribution(TEST_PROJECT_DISTRIBUTION_DATA)
- self.cmd = frontend.ExtractMessages(self.dist)
- self.cmd.initialize_options()
-
- def tearDown(self):
- if os.path.isfile(pot_file):
- os.unlink(pot_file)
-
- os.chdir(self.olddir)
-
- def assert_pot_file_exists(self):
- assert os.path.isfile(pot_file)
-
- def test_neither_default_nor_custom_keywords(self):
- self.cmd.output_file = 'dummy'
- self.cmd.no_default_keywords = True
- with pytest.raises(OptionError):
- self.cmd.finalize_options()
-
- def test_no_output_file_specified(self):
- with pytest.raises(OptionError):
- self.cmd.finalize_options()
-
- def test_both_sort_output_and_sort_by_file(self):
- self.cmd.output_file = 'dummy'
- self.cmd.sort_output = True
- self.cmd.sort_by_file = True
- with pytest.raises(OptionError):
- self.cmd.finalize_options()
-
- def test_invalid_file_or_dir_input_path(self):
- self.cmd.input_paths = 'nonexistent_path'
- self.cmd.output_file = 'dummy'
- with pytest.raises(OptionError):
- self.cmd.finalize_options()
-
- def test_input_paths_is_treated_as_list(self):
- self.cmd.input_paths = data_dir
- self.cmd.output_file = pot_file
- self.cmd.finalize_options()
- self.cmd.run()
-
- with open(pot_file) as f:
- catalog = read_po(f)
- msg = catalog.get('bar')
- assert len(msg.locations) == 1
- assert ('file1.py' in msg.locations[0][0])
-
- def test_input_paths_handle_spaces_after_comma(self):
- self.cmd.input_paths = f"{this_dir}, {data_dir}"
- self.cmd.output_file = pot_file
- self.cmd.finalize_options()
- assert self.cmd.input_paths == [this_dir, data_dir]
-
- def test_input_dirs_is_alias_for_input_paths(self):
- self.cmd.input_dirs = this_dir
- self.cmd.output_file = pot_file
- self.cmd.finalize_options()
- # Gets listified in `finalize_options`:
- assert self.cmd.input_paths == [self.cmd.input_dirs]
-
- def test_input_dirs_is_mutually_exclusive_with_input_paths(self):
- self.cmd.input_dirs = this_dir
- self.cmd.input_paths = this_dir
- self.cmd.output_file = pot_file
- with pytest.raises(OptionError):
- self.cmd.finalize_options()
-
- @freeze_time("1994-11-11")
- def test_extraction_with_default_mapping(self):
- self.cmd.copyright_holder = 'FooBar, Inc.'
- self.cmd.msgid_bugs_address = 'bugs.address@email.tld'
- self.cmd.output_file = 'project/i18n/temp.pot'
- self.cmd.add_comments = 'TRANSLATOR:,TRANSLATORS:'
-
- self.cmd.finalize_options()
- self.cmd.run()
-
- self.assert_pot_file_exists()
-
- date = format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en')
- expected_content = fr"""# Translations template for TestProject.
-# Copyright (C) {time.strftime('%Y')} FooBar, Inc.
-# This file is distributed under the same license as the TestProject
-# project.
-# FIRST AUTHOR <EMAIL@ADDRESS>, {time.strftime('%Y')}.
-#
-#, fuzzy
-msgid ""
-msgstr ""
-"Project-Id-Version: TestProject 0.1\n"
-"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
-"POT-Creation-Date: {date}\n"
-"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
-"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
-"Language-Team: LANGUAGE <LL@li.org>\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=utf-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel {VERSION}\n"
-
-#. TRANSLATOR: This will be a translator coment,
-#. that will include several lines
-#: project/file1.py:8
-msgid "bar"
-msgstr ""
-
-#: project/file2.py:9
-msgid "foobar"
-msgid_plural "foobars"
-msgstr[0] ""
-msgstr[1] ""
-
-#: project/ignored/this_wont_normally_be_here.py:11
-msgid "FooBar"
-msgid_plural "FooBars"
-msgstr[0] ""
-msgstr[1] ""
-
-"""
- with open(pot_file) as f:
- actual_content = f.read()
- assert expected_content == actual_content
-
- @freeze_time("1994-11-11")
- def test_extraction_with_mapping_file(self):
- self.cmd.copyright_holder = 'FooBar, Inc.'
- self.cmd.msgid_bugs_address = 'bugs.address@email.tld'
- self.cmd.mapping_file = 'mapping.cfg'
- self.cmd.output_file = 'project/i18n/temp.pot'
- self.cmd.add_comments = 'TRANSLATOR:,TRANSLATORS:'
-
- self.cmd.finalize_options()
- self.cmd.run()
-
- self.assert_pot_file_exists()
-
- date = format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en')
- expected_content = fr"""# Translations template for TestProject.
-# Copyright (C) {time.strftime('%Y')} FooBar, Inc.
-# This file is distributed under the same license as the TestProject
-# project.
-# FIRST AUTHOR <EMAIL@ADDRESS>, {time.strftime('%Y')}.
-#
-#, fuzzy
-msgid ""
-msgstr ""
-"Project-Id-Version: TestProject 0.1\n"
-"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
-"POT-Creation-Date: {date}\n"
-"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
-"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
-"Language-Team: LANGUAGE <LL@li.org>\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=utf-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel {VERSION}\n"
-
-#. TRANSLATOR: This will be a translator coment,
-#. that will include several lines
-#: project/file1.py:8
-msgid "bar"
-msgstr ""
-
-#: project/file2.py:9
-msgid "foobar"
-msgid_plural "foobars"
-msgstr[0] ""
-msgstr[1] ""
-
-"""
- with open(pot_file) as f:
- actual_content = f.read()
- assert expected_content == actual_content
-
- @freeze_time("1994-11-11")
- def test_extraction_with_mapping_dict(self):
- self.dist.message_extractors = {
- 'project': [
- ('**/ignored/**.*', 'ignore', None),
- ('**.py', 'python', None),
- ],
- }
- self.cmd.copyright_holder = 'FooBar, Inc.'
- self.cmd.msgid_bugs_address = 'bugs.address@email.tld'
- self.cmd.output_file = 'project/i18n/temp.pot'
- self.cmd.add_comments = 'TRANSLATOR:,TRANSLATORS:'
-
- self.cmd.finalize_options()
- self.cmd.run()
-
- self.assert_pot_file_exists()
-
- date = format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en')
- expected_content = fr"""# Translations template for TestProject.
-# Copyright (C) {time.strftime('%Y')} FooBar, Inc.
-# This file is distributed under the same license as the TestProject
-# project.
-# FIRST AUTHOR <EMAIL@ADDRESS>, {time.strftime('%Y')}.
-#
-#, fuzzy
-msgid ""
-msgstr ""
-"Project-Id-Version: TestProject 0.1\n"
-"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
-"POT-Creation-Date: {date}\n"
-"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
-"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
-"Language-Team: LANGUAGE <LL@li.org>\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=utf-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel {VERSION}\n"
-
-#. TRANSLATOR: This will be a translator coment,
-#. that will include several lines
-#: project/file1.py:8
-msgid "bar"
-msgstr ""
-
-#: project/file2.py:9
-msgid "foobar"
-msgid_plural "foobars"
-msgstr[0] ""
-msgstr[1] ""
-
-"""
- with open(pot_file) as f:
- actual_content = f.read()
- assert expected_content == actual_content
-
- def test_extraction_add_location_file(self):
- self.dist.message_extractors = {
- 'project': [
- ('**/ignored/**.*', 'ignore', None),
- ('**.py', 'python', None),
- ],
- }
- self.cmd.output_file = 'project/i18n/temp.pot'
- self.cmd.add_location = 'file'
- self.cmd.omit_header = True
-
- self.cmd.finalize_options()
- self.cmd.run()
-
- self.assert_pot_file_exists()
-
- expected_content = r"""#: project/file1.py
-msgid "bar"
-msgstr ""
-
-#: project/file2.py
-msgid "foobar"
-msgid_plural "foobars"
-msgstr[0] ""
-msgstr[1] ""
-
-"""
- with open(pot_file) as f:
- actual_content = f.read()
- assert expected_content == actual_content
-
-
-class InitCatalogTestCase(unittest.TestCase):
-
- def setUp(self):
- self.olddir = os.getcwd()
- os.chdir(data_dir)
-
- self.dist = Distribution(TEST_PROJECT_DISTRIBUTION_DATA)
- self.cmd = frontend.InitCatalog(self.dist)
- self.cmd.initialize_options()
-
- def tearDown(self):
- for dirname in ['en_US', 'ja_JP', 'lv_LV']:
- locale_dir = os.path.join(i18n_dir, dirname)
- if os.path.isdir(locale_dir):
- shutil.rmtree(locale_dir)
-
- os.chdir(self.olddir)
-
- def test_no_input_file(self):
- self.cmd.locale = 'en_US'
- self.cmd.output_file = 'dummy'
- with pytest.raises(OptionError):
- self.cmd.finalize_options()
-
- def test_no_locale(self):
- self.cmd.input_file = 'dummy'
- self.cmd.output_file = 'dummy'
- with pytest.raises(OptionError):
- self.cmd.finalize_options()
-
- @freeze_time("1994-11-11")
- def test_with_output_dir(self):
- self.cmd.input_file = 'project/i18n/messages.pot'
- self.cmd.locale = 'en_US'
- self.cmd.output_dir = 'project/i18n'
-
- self.cmd.finalize_options()
- self.cmd.run()
-
- po_file = _po_file('en_US')
- assert os.path.isfile(po_file)
-
- date = format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en')
- expected_content = fr"""# English (United States) translations for TestProject.
-# Copyright (C) 2007 FooBar, Inc.
-# This file is distributed under the same license as the TestProject
-# project.
-# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: TestProject 0.1\n"
-"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
-"POT-Creation-Date: 2007-04-01 15:30+0200\n"
-"PO-Revision-Date: {date}\n"
-"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
-"Language: en_US\n"
-"Language-Team: en_US <LL@li.org>\n"
-"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=utf-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel {VERSION}\n"
-
-#. This will be a translator coment,
-#. that will include several lines
-#: project/file1.py:8
-msgid "bar"
-msgstr ""
-
-#: project/file2.py:9
-msgid "foobar"
-msgid_plural "foobars"
-msgstr[0] ""
-msgstr[1] ""
-
-"""
- with open(po_file) as f:
- actual_content = f.read()
- assert expected_content == actual_content
-
- @freeze_time("1994-11-11")
- def test_keeps_catalog_non_fuzzy(self):
- self.cmd.input_file = 'project/i18n/messages_non_fuzzy.pot'
- self.cmd.locale = 'en_US'
- self.cmd.output_dir = 'project/i18n'
-
- self.cmd.finalize_options()
- self.cmd.run()
-
- po_file = _po_file('en_US')
- assert os.path.isfile(po_file)
-
- date = format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en')
- expected_content = fr"""# English (United States) translations for TestProject.
-# Copyright (C) 2007 FooBar, Inc.
-# This file is distributed under the same license as the TestProject
-# project.
-# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: TestProject 0.1\n"
-"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
-"POT-Creation-Date: 2007-04-01 15:30+0200\n"
-"PO-Revision-Date: {date}\n"
-"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
-"Language: en_US\n"
-"Language-Team: en_US <LL@li.org>\n"
-"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=utf-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel {VERSION}\n"
-
-#. This will be a translator coment,
-#. that will include several lines
-#: project/file1.py:8
-msgid "bar"
-msgstr ""
-
-#: project/file2.py:9
-msgid "foobar"
-msgid_plural "foobars"
-msgstr[0] ""
-msgstr[1] ""
-
-"""
- with open(po_file) as f:
- actual_content = f.read()
- assert expected_content == actual_content
-
- @freeze_time("1994-11-11")
- def test_correct_init_more_than_2_plurals(self):
- self.cmd.input_file = 'project/i18n/messages.pot'
- self.cmd.locale = 'lv_LV'
- self.cmd.output_dir = 'project/i18n'
-
- self.cmd.finalize_options()
- self.cmd.run()
-
- po_file = _po_file('lv_LV')
- assert os.path.isfile(po_file)
-
- date = format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en')
- expected_content = fr"""# Latvian (Latvia) translations for TestProject.
-# Copyright (C) 2007 FooBar, Inc.
-# This file is distributed under the same license as the TestProject
-# project.
-# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: TestProject 0.1\n"
-"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
-"POT-Creation-Date: 2007-04-01 15:30+0200\n"
-"PO-Revision-Date: {date}\n"
-"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
-"Language: lv_LV\n"
-"Language-Team: lv_LV <LL@li.org>\n"
-"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 :"
-" 2);\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=utf-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel {VERSION}\n"
-
-#. This will be a translator coment,
-#. that will include several lines
-#: project/file1.py:8
-msgid "bar"
-msgstr ""
-
-#: project/file2.py:9
-msgid "foobar"
-msgid_plural "foobars"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-
-"""
- with open(po_file) as f:
- actual_content = f.read()
- assert expected_content == actual_content
-
- @freeze_time("1994-11-11")
- def test_correct_init_singular_plural_forms(self):
- self.cmd.input_file = 'project/i18n/messages.pot'
- self.cmd.locale = 'ja_JP'
- self.cmd.output_dir = 'project/i18n'
-
- self.cmd.finalize_options()
- self.cmd.run()
-
- po_file = _po_file('ja_JP')
- assert os.path.isfile(po_file)
-
- date = format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='ja_JP')
- expected_content = fr"""# Japanese (Japan) translations for TestProject.
-# Copyright (C) 2007 FooBar, Inc.
-# This file is distributed under the same license as the TestProject
-# project.
-# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: TestProject 0.1\n"
-"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
-"POT-Creation-Date: 2007-04-01 15:30+0200\n"
-"PO-Revision-Date: {date}\n"
-"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
-"Language: ja_JP\n"
-"Language-Team: ja_JP <LL@li.org>\n"
-"Plural-Forms: nplurals=1; plural=0;\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=utf-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel {VERSION}\n"
-
-#. This will be a translator coment,
-#. that will include several lines
-#: project/file1.py:8
-msgid "bar"
-msgstr ""
-
-#: project/file2.py:9
-msgid "foobar"
-msgid_plural "foobars"
-msgstr[0] ""
-
-"""
- with open(po_file) as f:
- actual_content = f.read()
- assert expected_content == actual_content
-
- @freeze_time("1994-11-11")
- def test_supports_no_wrap(self):
- self.cmd.input_file = 'project/i18n/long_messages.pot'
- self.cmd.locale = 'en_US'
- self.cmd.output_dir = 'project/i18n'
-
- long_message = '"' + 'xxxxx ' * 15 + '"'
-
- with open('project/i18n/messages.pot', 'rb') as f:
- pot_contents = f.read().decode('latin-1')
- pot_with_very_long_line = pot_contents.replace('"bar"', long_message)
- with open(self.cmd.input_file, 'wb') as f:
- f.write(pot_with_very_long_line.encode('latin-1'))
- self.cmd.no_wrap = True
-
- self.cmd.finalize_options()
- self.cmd.run()
-
- po_file = _po_file('en_US')
- assert os.path.isfile(po_file)
- date = format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en_US')
- expected_content = fr"""# English (United States) translations for TestProject.
-# Copyright (C) 2007 FooBar, Inc.
-# This file is distributed under the same license as the TestProject
-# project.
-# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: TestProject 0.1\n"
-"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
-"POT-Creation-Date: 2007-04-01 15:30+0200\n"
-"PO-Revision-Date: {date}\n"
-"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
-"Language: en_US\n"
-"Language-Team: en_US <LL@li.org>\n"
-"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=utf-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel {VERSION}\n"
-
-#. This will be a translator coment,
-#. that will include several lines
-#: project/file1.py:8
-msgid {long_message}
-msgstr ""
-
-#: project/file2.py:9
-msgid "foobar"
-msgid_plural "foobars"
-msgstr[0] ""
-msgstr[1] ""
-
-"""
- with open(po_file) as f:
- actual_content = f.read()
- assert expected_content == actual_content
-
- @freeze_time("1994-11-11")
- def test_supports_width(self):
- self.cmd.input_file = 'project/i18n/long_messages.pot'
- self.cmd.locale = 'en_US'
- self.cmd.output_dir = 'project/i18n'
-
- long_message = '"' + 'xxxxx ' * 15 + '"'
-
- with open('project/i18n/messages.pot', 'rb') as f:
- pot_contents = f.read().decode('latin-1')
- pot_with_very_long_line = pot_contents.replace('"bar"', long_message)
- with open(self.cmd.input_file, 'wb') as f:
- f.write(pot_with_very_long_line.encode('latin-1'))
- self.cmd.width = 120
- self.cmd.finalize_options()
- self.cmd.run()
-
- po_file = _po_file('en_US')
- assert os.path.isfile(po_file)
- date = format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en_US')
- expected_content = fr"""# English (United States) translations for TestProject.
-# Copyright (C) 2007 FooBar, Inc.
-# This file is distributed under the same license as the TestProject
-# project.
-# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: TestProject 0.1\n"
-"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
-"POT-Creation-Date: 2007-04-01 15:30+0200\n"
-"PO-Revision-Date: {date}\n"
-"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
-"Language: en_US\n"
-"Language-Team: en_US <LL@li.org>\n"
-"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=utf-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel {VERSION}\n"
-
-#. This will be a translator coment,
-#. that will include several lines
-#: project/file1.py:8
-msgid {long_message}
-msgstr ""
-
-#: project/file2.py:9
-msgid "foobar"
-msgid_plural "foobars"
-msgstr[0] ""
-msgstr[1] ""
-
-"""
- with open(po_file) as f:
- actual_content = f.read()
- assert expected_content == actual_content
-
-
-class CommandLineInterfaceTestCase(unittest.TestCase):
-
- def setUp(self):
- data_dir = os.path.join(this_dir, 'data')
- self.orig_working_dir = os.getcwd()
- self.orig_argv = sys.argv
- self.orig_stdout = sys.stdout
- self.orig_stderr = sys.stderr
- sys.argv = ['pybabel']
- sys.stdout = StringIO()
- sys.stderr = StringIO()
- os.chdir(data_dir)
-
- self._remove_log_handlers()
- self.cli = frontend.CommandLineInterface()
-
- def tearDown(self):
- os.chdir(self.orig_working_dir)
- sys.argv = self.orig_argv
- sys.stdout = self.orig_stdout
- sys.stderr = self.orig_stderr
- for dirname in ['lv_LV', 'ja_JP']:
- locale_dir = os.path.join(i18n_dir, dirname)
- if os.path.isdir(locale_dir):
- shutil.rmtree(locale_dir)
- self._remove_log_handlers()
-
- def _remove_log_handlers(self):
- # Logging handlers will be reused if possible (#227). This breaks the
- # implicit assumption that our newly created StringIO for sys.stderr
- # contains the console output. Removing the old handler ensures that a
- # new handler with our new StringIO instance will be used.
- log = logging.getLogger('babel')
- for handler in log.handlers:
- log.removeHandler(handler)
-
- def test_usage(self):
- try:
- self.cli.run(sys.argv)
- self.fail('Expected SystemExit')
- except SystemExit as e:
- assert e.code == 2
- assert sys.stderr.getvalue().lower() == """\
-usage: pybabel command [options] [args]
-
-pybabel: error: no valid command or option passed. try the -h/--help option for more information.
-"""
-
- def test_list_locales(self):
- """
- Test the command with the --list-locales arg.
- """
- result = self.cli.run(sys.argv + ['--list-locales'])
- assert not result
- output = sys.stdout.getvalue()
- assert 'fr_CH' in output
- assert 'French (Switzerland)' in output
- assert "\nb'" not in output # No bytes repr markers in output
-
- def _run_init_catalog(self):
- i18n_dir = os.path.join(data_dir, 'project', 'i18n')
- pot_path = os.path.join(data_dir, 'project', 'i18n', 'messages.pot')
- init_argv = sys.argv + ['init', '--locale', 'en_US', '-d', i18n_dir,
- '-i', pot_path]
- self.cli.run(init_argv)
-
- def test_no_duplicated_output_for_multiple_runs(self):
- self._run_init_catalog()
- first_output = sys.stderr.getvalue()
- self._run_init_catalog()
- second_output = sys.stderr.getvalue()[len(first_output):]
-
- # in case the log message is not duplicated we should get the same
- # output as before
- assert first_output == second_output
-
- def test_frontend_can_log_to_predefined_handler(self):
- custom_stream = StringIO()
- log = logging.getLogger('babel')
- log.addHandler(logging.StreamHandler(custom_stream))
-
- self._run_init_catalog()
- assert id(sys.stderr) != id(custom_stream)
- assert not sys.stderr.getvalue()
- assert custom_stream.getvalue()
-
- def test_help(self):
- try:
- self.cli.run(sys.argv + ['--help'])
- self.fail('Expected SystemExit')
- except SystemExit as e:
- assert not e.code
- content = sys.stdout.getvalue().lower()
- assert 'options:' in content
- assert all(command in content for command in ('init', 'update', 'compile', 'extract'))
-
- def assert_pot_file_exists(self):
- assert os.path.isfile(pot_file)
-
- @freeze_time("1994-11-11")
- def test_extract_with_default_mapping(self):
- self.cli.run(sys.argv + ['extract',
- '--copyright-holder', 'FooBar, Inc.',
- '--project', 'TestProject', '--version', '0.1',
- '--msgid-bugs-address', 'bugs.address@email.tld',
- '-c', 'TRANSLATOR', '-c', 'TRANSLATORS:',
- '-o', pot_file, 'project'])
- self.assert_pot_file_exists()
- date = format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en')
- expected_content = fr"""# Translations template for TestProject.
-# Copyright (C) {time.strftime('%Y')} FooBar, Inc.
-# This file is distributed under the same license as the TestProject
-# project.
-# FIRST AUTHOR <EMAIL@ADDRESS>, {time.strftime('%Y')}.
-#
-#, fuzzy
-msgid ""
-msgstr ""
-"Project-Id-Version: TestProject 0.1\n"
-"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
-"POT-Creation-Date: {date}\n"
-"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
-"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
-"Language-Team: LANGUAGE <LL@li.org>\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=utf-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel {VERSION}\n"
-
-#. TRANSLATOR: This will be a translator coment,
-#. that will include several lines
-#: project/file1.py:8
-msgid "bar"
-msgstr ""
-
-#: project/file2.py:9
-msgid "foobar"
-msgid_plural "foobars"
-msgstr[0] ""
-msgstr[1] ""
-
-#: project/ignored/this_wont_normally_be_here.py:11
-msgid "FooBar"
-msgid_plural "FooBars"
-msgstr[0] ""
-msgstr[1] ""
-
-"""
- with open(pot_file) as f:
- actual_content = f.read()
- assert expected_content == actual_content
-
- @freeze_time("1994-11-11")
- def test_extract_with_mapping_file(self):
- self.cli.run(sys.argv + ['extract',
- '--copyright-holder', 'FooBar, Inc.',
- '--project', 'TestProject', '--version', '0.1',
- '--msgid-bugs-address', 'bugs.address@email.tld',
- '--mapping', os.path.join(data_dir, 'mapping.cfg'),
- '-c', 'TRANSLATOR', '-c', 'TRANSLATORS:',
- '-o', pot_file, 'project'])
- self.assert_pot_file_exists()
- date = format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en')
- expected_content = fr"""# Translations template for TestProject.
-# Copyright (C) {time.strftime('%Y')} FooBar, Inc.
-# This file is distributed under the same license as the TestProject
-# project.
-# FIRST AUTHOR <EMAIL@ADDRESS>, {time.strftime('%Y')}.
-#
-#, fuzzy
-msgid ""
-msgstr ""
-"Project-Id-Version: TestProject 0.1\n"
-"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
-"POT-Creation-Date: {date}\n"
-"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
-"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
-"Language-Team: LANGUAGE <LL@li.org>\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=utf-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel {VERSION}\n"
-
-#. TRANSLATOR: This will be a translator coment,
-#. that will include several lines
-#: project/file1.py:8
-msgid "bar"
-msgstr ""
-
-#: project/file2.py:9
-msgid "foobar"
-msgid_plural "foobars"
-msgstr[0] ""
-msgstr[1] ""
-
-"""
- with open(pot_file) as f:
- actual_content = f.read()
- assert expected_content == actual_content
-
- @freeze_time("1994-11-11")
- def test_extract_with_exact_file(self):
- """Tests that we can call extract with a particular file and only
- strings from that file get extracted. (Note the absence of strings from file1.py)
- """
- file_to_extract = os.path.join(data_dir, 'project', 'file2.py')
- self.cli.run(sys.argv + ['extract',
- '--copyright-holder', 'FooBar, Inc.',
- '--project', 'TestProject', '--version', '0.1',
- '--msgid-bugs-address', 'bugs.address@email.tld',
- '--mapping', os.path.join(data_dir, 'mapping.cfg'),
- '-c', 'TRANSLATOR', '-c', 'TRANSLATORS:',
- '-o', pot_file, file_to_extract])
- self.assert_pot_file_exists()
- date = format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en')
- expected_content = fr"""# Translations template for TestProject.
-# Copyright (C) {time.strftime('%Y')} FooBar, Inc.
-# This file is distributed under the same license as the TestProject
-# project.
-# FIRST AUTHOR <EMAIL@ADDRESS>, {time.strftime('%Y')}.
-#
-#, fuzzy
-msgid ""
-msgstr ""
-"Project-Id-Version: TestProject 0.1\n"
-"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
-"POT-Creation-Date: {date}\n"
-"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
-"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
-"Language-Team: LANGUAGE <LL@li.org>\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=utf-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel {VERSION}\n"
-
-#: project/file2.py:9
-msgid "foobar"
-msgid_plural "foobars"
-msgstr[0] ""
-msgstr[1] ""
-
-"""
- with open(pot_file) as f:
- actual_content = f.read()
- assert expected_content == actual_content
-
- @freeze_time("1994-11-11")
- def test_init_with_output_dir(self):
- po_file = _po_file('en_US')
- self.cli.run(sys.argv + ['init',
- '--locale', 'en_US',
- '-d', os.path.join(i18n_dir),
- '-i', os.path.join(i18n_dir, 'messages.pot')])
- assert os.path.isfile(po_file)
- date = format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en')
- expected_content = fr"""# English (United States) translations for TestProject.
-# Copyright (C) 2007 FooBar, Inc.
-# This file is distributed under the same license as the TestProject
-# project.
-# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: TestProject 0.1\n"
-"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
-"POT-Creation-Date: 2007-04-01 15:30+0200\n"
-"PO-Revision-Date: {date}\n"
-"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
-"Language: en_US\n"
-"Language-Team: en_US <LL@li.org>\n"
-"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=utf-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel {VERSION}\n"
-
-#. This will be a translator coment,
-#. that will include several lines
-#: project/file1.py:8
-msgid "bar"
-msgstr ""
-
-#: project/file2.py:9
-msgid "foobar"
-msgid_plural "foobars"
-msgstr[0] ""
-msgstr[1] ""
-
-"""
- with open(po_file) as f:
- actual_content = f.read()
- assert expected_content == actual_content
-
- @freeze_time("1994-11-11")
- def test_init_singular_plural_forms(self):
- po_file = _po_file('ja_JP')
- self.cli.run(sys.argv + ['init',
- '--locale', 'ja_JP',
- '-d', os.path.join(i18n_dir),
- '-i', os.path.join(i18n_dir, 'messages.pot')])
- assert os.path.isfile(po_file)
- date = format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en')
- expected_content = fr"""# Japanese (Japan) translations for TestProject.
-# Copyright (C) 2007 FooBar, Inc.
-# This file is distributed under the same license as the TestProject
-# project.
-# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: TestProject 0.1\n"
-"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
-"POT-Creation-Date: 2007-04-01 15:30+0200\n"
-"PO-Revision-Date: {date}\n"
-"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
-"Language: ja_JP\n"
-"Language-Team: ja_JP <LL@li.org>\n"
-"Plural-Forms: nplurals=1; plural=0;\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=utf-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel {VERSION}\n"
-
-#. This will be a translator coment,
-#. that will include several lines
-#: project/file1.py:8
-msgid "bar"
-msgstr ""
-
-#: project/file2.py:9
-msgid "foobar"
-msgid_plural "foobars"
-msgstr[0] ""
-
-"""
- with open(po_file) as f:
- actual_content = f.read()
- assert expected_content == actual_content
-
- @freeze_time("1994-11-11")
- def test_init_more_than_2_plural_forms(self):
- po_file = _po_file('lv_LV')
- self.cli.run(sys.argv + ['init',
- '--locale', 'lv_LV',
- '-d', i18n_dir,
- '-i', os.path.join(i18n_dir, 'messages.pot')])
- assert os.path.isfile(po_file)
- date = format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en')
- expected_content = fr"""# Latvian (Latvia) translations for TestProject.
-# Copyright (C) 2007 FooBar, Inc.
-# This file is distributed under the same license as the TestProject
-# project.
-# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: TestProject 0.1\n"
-"Report-Msgid-Bugs-To: bugs.address@email.tld\n"
-"POT-Creation-Date: 2007-04-01 15:30+0200\n"
-"PO-Revision-Date: {date}\n"
-"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
-"Language: lv_LV\n"
-"Language-Team: lv_LV <LL@li.org>\n"
-"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 :"
-" 2);\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=utf-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel {VERSION}\n"
-
-#. This will be a translator coment,
-#. that will include several lines
-#: project/file1.py:8
-msgid "bar"
-msgstr ""
-
-#: project/file2.py:9
-msgid "foobar"
-msgid_plural "foobars"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-
-"""
- with open(po_file) as f:
- actual_content = f.read()
- assert expected_content == actual_content
-
- def test_compile_catalog(self):
- po_file = _po_file('de_DE')
- mo_file = po_file.replace('.po', '.mo')
- self.cli.run(sys.argv + ['compile',
- '--locale', 'de_DE',
- '-d', i18n_dir])
- assert not os.path.isfile(mo_file), f'Expected no file at {mo_file!r}'
- assert sys.stderr.getvalue() == f'catalog {po_file} is marked as fuzzy, skipping\n'
-
- def test_compile_fuzzy_catalog(self):
- po_file = _po_file('de_DE')
- mo_file = po_file.replace('.po', '.mo')
- try:
- self.cli.run(sys.argv + ['compile',
- '--locale', 'de_DE', '--use-fuzzy',
- '-d', i18n_dir])
- assert os.path.isfile(mo_file)
- assert sys.stderr.getvalue() == f'compiling catalog {po_file} to {mo_file}\n'
- finally:
- if os.path.isfile(mo_file):
- os.unlink(mo_file)
-
- def test_compile_catalog_with_more_than_2_plural_forms(self):
- po_file = _po_file('ru_RU')
- mo_file = po_file.replace('.po', '.mo')
- try:
- self.cli.run(sys.argv + ['compile',
- '--locale', 'ru_RU', '--use-fuzzy',
- '-d', i18n_dir])
- assert os.path.isfile(mo_file)
- assert sys.stderr.getvalue() == f'compiling catalog {po_file} to {mo_file}\n'
- finally:
- if os.path.isfile(mo_file):
- os.unlink(mo_file)
-
- def test_compile_catalog_multidomain(self):
- po_foo = os.path.join(i18n_dir, 'de_DE', 'LC_MESSAGES', 'foo.po')
- po_bar = os.path.join(i18n_dir, 'de_DE', 'LC_MESSAGES', 'bar.po')
- mo_foo = po_foo.replace('.po', '.mo')
- mo_bar = po_bar.replace('.po', '.mo')
- try:
- self.cli.run(sys.argv + ['compile',
- '--locale', 'de_DE', '--domain', 'foo bar', '--use-fuzzy',
- '-d', i18n_dir])
- for mo_file in [mo_foo, mo_bar]:
- assert os.path.isfile(mo_file)
- assert sys.stderr.getvalue() == (
- f'compiling catalog {po_foo} to {mo_foo}\n'
- f'compiling catalog {po_bar} to {mo_bar}\n'
- )
-
- finally:
- for mo_file in [mo_foo, mo_bar]:
- if os.path.isfile(mo_file):
- os.unlink(mo_file)
-
- def test_update(self):
- template = Catalog()
- template.add("1")
- template.add("2")
- template.add("3")
- tmpl_file = os.path.join(i18n_dir, 'temp-template.pot')
- with open(tmpl_file, "wb") as outfp:
- write_po(outfp, template)
- po_file = os.path.join(i18n_dir, 'temp1.po')
- self.cli.run(sys.argv + ['init',
- '-l', 'fi',
- '-o', po_file,
- '-i', tmpl_file,
- ])
- with open(po_file) as infp:
- catalog = read_po(infp)
- assert len(catalog) == 3
-
- # Add another entry to the template
-
- template.add("4")
-
- with open(tmpl_file, "wb") as outfp:
- write_po(outfp, template)
-
- self.cli.run(sys.argv + ['update',
- '-l', 'fi_FI',
- '-o', po_file,
- '-i', tmpl_file])
-
- with open(po_file) as infp:
- catalog = read_po(infp)
- assert len(catalog) == 4 # Catalog was updated
-
- def test_update_pot_creation_date(self):
- template = Catalog()
- template.add("1")
- template.add("2")
- template.add("3")
- tmpl_file = os.path.join(i18n_dir, 'temp-template.pot')
- with open(tmpl_file, "wb") as outfp:
- write_po(outfp, template)
- po_file = os.path.join(i18n_dir, 'temp1.po')
- self.cli.run(sys.argv + ['init',
- '-l', 'fi',
- '-o', po_file,
- '-i', tmpl_file,
- ])
- with open(po_file) as infp:
- catalog = read_po(infp)
- assert len(catalog) == 3
- original_catalog_creation_date = catalog.creation_date
-
- # Update the template creation date
- template.creation_date -= timedelta(minutes=3)
- with open(tmpl_file, "wb") as outfp:
- write_po(outfp, template)
-
- self.cli.run(sys.argv + ['update',
- '-l', 'fi_FI',
- '-o', po_file,
- '-i', tmpl_file])
-
- with open(po_file) as infp:
- catalog = read_po(infp)
- # We didn't ignore the creation date, so expect a diff
- assert catalog.creation_date != original_catalog_creation_date
-
- # Reset the "original"
- original_catalog_creation_date = catalog.creation_date
-
- # Update the template creation date again
- # This time, pass the ignore flag and expect the times are different
- template.creation_date -= timedelta(minutes=5)
- with open(tmpl_file, "wb") as outfp:
- write_po(outfp, template)
-
- self.cli.run(sys.argv + ['update',
- '-l', 'fi_FI',
- '-o', po_file,
- '-i', tmpl_file,
- '--ignore-pot-creation-date'])
-
- with open(po_file) as infp:
- catalog = read_po(infp)
- # We ignored creation date, so it should not have changed
- assert catalog.creation_date == original_catalog_creation_date
-
- def test_check(self):
- template = Catalog()
- template.add("1")
- template.add("2")
- template.add("3")
- tmpl_file = os.path.join(i18n_dir, 'temp-template.pot')
- with open(tmpl_file, "wb") as outfp:
- write_po(outfp, template)
- po_file = os.path.join(i18n_dir, 'temp1.po')
- self.cli.run(sys.argv + ['init',
- '-l', 'fi_FI',
- '-o', po_file,
- '-i', tmpl_file,
- ])
-
- # Update the catalog file
- self.cli.run(sys.argv + ['update',
- '-l', 'fi_FI',
- '-o', po_file,
- '-i', tmpl_file])
-
- # Run a check without introducing any changes to the template
- self.cli.run(sys.argv + ['update',
- '--check',
- '-l', 'fi_FI',
- '-o', po_file,
- '-i', tmpl_file])
-
- # Add a new entry and expect the check to fail
- template.add("4")
- with open(tmpl_file, "wb") as outfp:
- write_po(outfp, template)
-
- with pytest.raises(BaseError):
- self.cli.run(sys.argv + ['update',
- '--check',
- '-l', 'fi_FI',
- '-o', po_file,
- '-i', tmpl_file])
-
- # Write the latest changes to the po-file
- self.cli.run(sys.argv + ['update',
- '-l', 'fi_FI',
- '-o', po_file,
- '-i', tmpl_file])
-
- # Update an entry and expect the check to fail
- template.add("4", locations=[("foo.py", 1)])
- with open(tmpl_file, "wb") as outfp:
- write_po(outfp, template)
-
- with pytest.raises(BaseError):
- self.cli.run(sys.argv + ['update',
- '--check',
- '-l', 'fi_FI',
- '-o', po_file,
- '-i', tmpl_file])
-
- def test_check_pot_creation_date(self):
- template = Catalog()
- template.add("1")
- template.add("2")
- template.add("3")
- tmpl_file = os.path.join(i18n_dir, 'temp-template.pot')
- with open(tmpl_file, "wb") as outfp:
- write_po(outfp, template)
- po_file = os.path.join(i18n_dir, 'temp1.po')
- self.cli.run(sys.argv + ['init',
- '-l', 'fi_FI',
- '-o', po_file,
- '-i', tmpl_file,
- ])
-
- # Update the catalog file
- self.cli.run(sys.argv + ['update',
- '-l', 'fi_FI',
- '-o', po_file,
- '-i', tmpl_file])
-
- # Run a check without introducing any changes to the template
- self.cli.run(sys.argv + ['update',
- '--check',
- '-l', 'fi_FI',
- '-o', po_file,
- '-i', tmpl_file])
-
- # Run a check after changing the template creation date
- template.creation_date = datetime.now() - timedelta(minutes=5)
- with open(tmpl_file, "wb") as outfp:
- write_po(outfp, template)
-
- # Should fail without --ignore-pot-creation-date flag
- with pytest.raises(BaseError):
- self.cli.run(sys.argv + ['update',
- '--check',
- '-l', 'fi_FI',
- '-o', po_file,
- '-i', tmpl_file])
- # Should pass with --ignore-pot-creation-date flag
- self.cli.run(sys.argv + ['update',
- '--check',
- '-l', 'fi_FI',
- '-o', po_file,
- '-i', tmpl_file,
- '--ignore-pot-creation-date'])
-
- def test_update_init_missing(self):
- template = Catalog()
- template.add("1")
- template.add("2")
- template.add("3")
- tmpl_file = os.path.join(i18n_dir, 'temp2-template.pot')
- with open(tmpl_file, "wb") as outfp:
- write_po(outfp, template)
- po_file = os.path.join(i18n_dir, 'temp2.po')
-
- self.cli.run(sys.argv + ['update',
- '--init-missing',
- '-l', 'fi',
- '-o', po_file,
- '-i', tmpl_file])
-
- with open(po_file) as infp:
- catalog = read_po(infp)
- assert len(catalog) == 3
-
- # Add another entry to the template
-
- template.add("4")
-
- with open(tmpl_file, "wb") as outfp:
- write_po(outfp, template)
-
- self.cli.run(sys.argv + ['update',
- '--init-missing',
- '-l', 'fi_FI',
- '-o', po_file,
- '-i', tmpl_file])
-
- with open(po_file) as infp:
- catalog = read_po(infp)
- assert len(catalog) == 4 # Catalog was updated
-
-
-mapping_cfg = """
-[extractors]
-custom = tests.messages.utils:custom_extractor
-
-# Special extractor for a given Python file
-[custom: special.py]
-treat = delicious
-
-# Python source files
-[python: **.py]
-
-# Genshi templates
-[genshi: **/templates/**.html]
-include_attrs =
-
-[genshi: **/templates/**.txt]
-template_class = genshi.template:TextTemplate
-encoding = latin-1
-
-# Some custom extractor
-[custom: **/custom/*.*]
-"""
-
-mapping_toml = """
-[extractors]
-custom = "tests.messages.utils:custom_extractor"
-
-# Special extractor for a given Python file
-[[mappings]]
-method = "custom"
-pattern = "special.py"
-treat = "delightful"
-
-# Python source files
-[[mappings]]
-method = "python"
-pattern = "**.py"
-
-# Genshi templates
-[[mappings]]
-method = "genshi"
-pattern = "**/templates/**.html"
-include_attrs = ""
-
-[[mappings]]
-method = "genshi"
-pattern = "**/templates/**.txt"
-template_class = "genshi.template:TextTemplate"
-encoding = "latin-1"
-
-# Some custom extractor
-[[mappings]]
-method = "custom"
-pattern = "**/custom/*.*"
-"""
-
-
-@pytest.mark.parametrize(
- ("data", "parser", "preprocess", "is_toml"),
- [
- (
- mapping_cfg,
- frontend.parse_mapping_cfg,
- None,
- False,
- ),
- (
- mapping_toml,
- frontend._parse_mapping_toml,
- None,
- True,
- ),
- (
- mapping_toml,
- partial(frontend._parse_mapping_toml, style="pyproject.toml"),
- lambda s: re.sub(r"^(\[+)", r"\1tool.babel.", s, flags=re.MULTILINE),
- True,
- ),
- ],
- ids=("cfg", "toml", "pyproject-toml"),
-)
-def test_parse_mapping(data: str, parser, preprocess, is_toml):
- if preprocess:
- data = preprocess(data)
- if is_toml:
- buf = BytesIO(data.encode())
- else:
- buf = StringIO(data)
-
- method_map, options_map = parser(buf)
- assert len(method_map) == 5
-
- assert method_map[1] == ('**.py', 'python')
- assert options_map['**.py'] == {}
- assert method_map[2] == ('**/templates/**.html', 'genshi')
- assert options_map['**/templates/**.html']['include_attrs'] == ''
- assert method_map[3] == ('**/templates/**.txt', 'genshi')
- assert (options_map['**/templates/**.txt']['template_class']
- == 'genshi.template:TextTemplate')
- assert options_map['**/templates/**.txt']['encoding'] == 'latin-1'
- assert method_map[4] == ('**/custom/*.*', 'tests.messages.utils:custom_extractor')
- assert options_map['**/custom/*.*'] == {}
-
-
-def test_parse_keywords():
- kw = frontend.parse_keywords(['_', 'dgettext:2',
- 'dngettext:2,3', 'pgettext:1c,2'])
- assert kw == {
- '_': None,
- 'dgettext': (2,),
- 'dngettext': (2, 3),
- 'pgettext': ((1, 'c'), 2),
- }
-
-
-def test_parse_keywords_with_t():
- kw = frontend.parse_keywords(['_:1', '_:2,2t', '_:2c,3,3t'])
-
- assert kw == {
- '_': {
- None: (1,),
- 2: (2,),
- 3: ((2, 'c'), 3),
- },
- }
-
-
-def test_extract_messages_with_t():
- content = rb"""
-_("1 arg, arg 1")
-_("2 args, arg 1", "2 args, arg 2")
-_("3 args, arg 1", "3 args, arg 2", "3 args, arg 3")
-_("4 args, arg 1", "4 args, arg 2", "4 args, arg 3", "4 args, arg 4")
-"""
- kw = frontend.parse_keywords(['_:1', '_:2,2t', '_:2c,3,3t'])
- result = list(extract.extract("python", BytesIO(content), kw))
- expected = [(2, '1 arg, arg 1', [], None),
- (3, '2 args, arg 1', [], None),
- (3, '2 args, arg 2', [], None),
- (4, '3 args, arg 1', [], None),
- (4, '3 args, arg 3', [], '3 args, arg 2'),
- (5, '4 args, arg 1', [], None)]
- assert result == expected
-
-
-def configure_cli_command(cmdline: str | list[str]):
- """
- Helper to configure a command class, but not run it just yet.
-
- :param cmdline: The command line (sans the executable name)
- :return: Command instance
- """
- args = shlex.split(cmdline) if isinstance(cmdline, str) else list(cmdline)
- cli = CommandLineInterface()
- cmdinst = cli._configure_command(cmdname=args[0], argv=args[1:])
- return cmdinst
-
-
-@pytest.mark.parametrize("split", (False, True))
-@pytest.mark.parametrize("arg_name", ("-k", "--keyword", "--keywords"))
-def test_extract_keyword_args_384(split, arg_name):
- # This is a regression test for https://github.com/python-babel/babel/issues/384
- # and it also tests that the rest of the forgotten aliases/shorthands implied by
- # https://github.com/python-babel/babel/issues/390 are re-remembered (or rather
- # that the mechanism for remembering them again works).
-
- kwarg_specs = [
- "gettext_noop",
- "gettext_lazy",
- "ngettext_lazy:1,2",
- "ugettext_noop",
- "ugettext_lazy",
- "ungettext_lazy:1,2",
- "pgettext_lazy:1c,2",
- "npgettext_lazy:1c,2,3",
- ]
-
- if split: # Generate a command line with multiple -ks
- kwarg_text = " ".join(f"{arg_name} {kwarg_spec}" for kwarg_spec in kwarg_specs)
- else: # Generate a single space-separated -k
- specs = ' '.join(kwarg_specs)
- kwarg_text = f'{arg_name} "{specs}"'
-
- # (Both of those invocation styles should be equivalent, so there is no parametrization from here on out)
-
- cmdinst = configure_cli_command(
- f"extract -F babel-django.cfg --add-comments Translators: -o django232.pot {kwarg_text} .",
- )
- assert isinstance(cmdinst, ExtractMessages)
- assert set(cmdinst.keywords.keys()) == {'_', 'dgettext', 'dngettext',
- 'gettext', 'gettext_lazy',
- 'gettext_noop', 'N_', 'ngettext',
- 'ngettext_lazy', 'npgettext',
- 'npgettext_lazy', 'pgettext',
- 'pgettext_lazy', 'ugettext',
- 'ugettext_lazy', 'ugettext_noop',
- 'ungettext', 'ungettext_lazy'}
-
-
-def test_update_catalog_boolean_args():
- cmdinst = configure_cli_command(
- "update --init-missing --no-wrap -N --ignore-obsolete --previous -i foo -o foo -l en")
- assert isinstance(cmdinst, UpdateCatalog)
- assert cmdinst.init_missing is True
- assert cmdinst.no_wrap is True
- assert cmdinst.no_fuzzy_matching is True
- assert cmdinst.ignore_obsolete is True
- assert cmdinst.previous is False # Mutually exclusive with no_fuzzy_matching
-
-
-
-def test_compile_catalog_dir(tmp_path):
- """
- Test that `compile` can compile all locales in a directory.
- """
- locales = ("fi_FI", "sv_SE")
- for locale in locales:
- l_dir = tmp_path / locale / "LC_MESSAGES"
- l_dir.mkdir(parents=True)
- po_file = l_dir / 'messages.po'
- po_file.write_text('msgid "foo"\nmsgstr "bar"\n')
- cmdinst = configure_cli_command([ # fmt: skip
- 'compile',
- '--statistics',
- '--use-fuzzy',
- '-d', str(tmp_path),
- ])
- assert not cmdinst.run()
- for locale in locales:
- assert (tmp_path / locale / "LC_MESSAGES" / "messages.mo").exists()
-
-
-def test_compile_catalog_explicit(tmp_path):
- """
- Test that `compile` can explicitly compile a single catalog.
- """
- po_file = tmp_path / 'temp.po'
- po_file.write_text('msgid "foo"\nmsgstr "bar"\n')
- mo_file = tmp_path / 'temp.mo'
- cmdinst = configure_cli_command([ # fmt: skip
- 'compile',
- '--statistics',
- '--use-fuzzy',
- '-i', str(po_file),
- '-o', str(mo_file),
- '-l', 'fi_FI',
- ])
- assert not cmdinst.run()
- assert mo_file.exists()
-
-
-
-@pytest.mark.parametrize("explicit_locale", (None, 'fi_FI'), ids=("implicit", "explicit"))
-def test_update_dir(tmp_path, explicit_locale: bool):
- """
- Test that `update` can deal with directories too.
- """
- template = Catalog()
- template.add("1")
- template.add("2")
- template.add("3")
- tmpl_file = (tmp_path / 'temp-template.pot')
- with tmpl_file.open("wb") as outfp:
- write_po(outfp, template)
- locales = ("fi_FI", "sv_SE")
- for locale in locales:
- l_dir = tmp_path / locale / "LC_MESSAGES"
- l_dir.mkdir(parents=True)
- po_file = l_dir / 'messages.po'
- po_file.touch()
- cmdinst = configure_cli_command([ # fmt: skip
- 'update',
- '-i', str(tmpl_file),
- '-d', str(tmp_path),
- *(['-l', explicit_locale] if explicit_locale else []),
- ])
- assert not cmdinst.run()
- for locale in locales:
- if explicit_locale and locale != explicit_locale:
- continue
- assert (tmp_path / locale / "LC_MESSAGES" / "messages.po").stat().st_size > 0
-
-
-def test_extract_cli_knows_dash_s():
- # This is a regression test for https://github.com/python-babel/babel/issues/390
- cmdinst = configure_cli_command("extract -s -o foo babel")
- assert isinstance(cmdinst, ExtractMessages)
- assert cmdinst.strip_comments
-
-
-def test_extract_cli_knows_dash_dash_last_dash_translator():
- cmdinst = configure_cli_command('extract --last-translator "FULL NAME EMAIL@ADDRESS" -o foo babel')
- assert isinstance(cmdinst, ExtractMessages)
- assert cmdinst.last_translator == "FULL NAME EMAIL@ADDRESS"
-
-
-def test_extract_add_location():
- cmdinst = configure_cli_command("extract -o foo babel --add-location full")
- assert isinstance(cmdinst, ExtractMessages)
- assert cmdinst.add_location == 'full'
- assert not cmdinst.no_location
- assert cmdinst.include_lineno
-
- cmdinst = configure_cli_command("extract -o foo babel --add-location file")
- assert isinstance(cmdinst, ExtractMessages)
- assert cmdinst.add_location == 'file'
- assert not cmdinst.no_location
- assert not cmdinst.include_lineno
-
- cmdinst = configure_cli_command("extract -o foo babel --add-location never")
- assert isinstance(cmdinst, ExtractMessages)
- assert cmdinst.add_location == 'never'
- assert cmdinst.no_location
-
-
-def test_extract_error_code(monkeypatch, capsys):
- monkeypatch.chdir(project_dir)
- cmdinst = configure_cli_command("compile --domain=messages --directory i18n --locale fi_BUGGY")
- assert cmdinst.run() == 1
- out, err = capsys.readouterr()
- if err:
- assert "unknown named placeholder 'merkki'" in err
-
-
-@pytest.mark.parametrize("with_underscore_ignore", (False, True))
-def test_extract_ignore_dirs(monkeypatch, capsys, tmp_path, with_underscore_ignore):
- pot_file = tmp_path / 'temp.pot'
- monkeypatch.chdir(project_dir)
- cmd = f"extract . -o '{pot_file}' --ignore-dirs '*ignored* .*' "
- if with_underscore_ignore:
- # This also tests that multiple arguments are supported.
- cmd += "--ignore-dirs '_*'"
- cmdinst = configure_cli_command(cmd)
- assert isinstance(cmdinst, ExtractMessages)
- assert cmdinst.directory_filter
- cmdinst.run()
- pot_content = pot_file.read_text()
-
- # The `ignored` directory is now actually ignored:
- assert 'this_wont_normally_be_here' not in pot_content
-
- # Since we manually set a filter, the otherwise `_hidden` directory is walked into,
- # unless we opt in to ignore it again
- assert ('ssshhh....' in pot_content) != with_underscore_ignore
- assert ('_hidden_by_default' in pot_content) != with_underscore_ignore
-
-
-def test_extract_header_comment(monkeypatch, tmp_path):
- pot_file = tmp_path / 'temp.pot'
- monkeypatch.chdir(project_dir)
- cmdinst = configure_cli_command(f"extract . -o '{pot_file}' --header-comment 'Boing' ")
- cmdinst.run()
- pot_content = pot_file.read_text()
- assert 'Boing' in pot_content
-
-
-@pytest.mark.parametrize("mapping_format", ("toml", "cfg"))
-def test_pr_1121(tmp_path, monkeypatch, caplog, mapping_format):
- """
- Test that extraction uses the first matching method and options,
- instead of the first matching method and last matching options.
-
- Without the fix in PR #1121, this test would fail,
- since the `custom_extractor` isn't passed a delicious treat via
- the configuration.
- """
- if mapping_format == "cfg":
- mapping_file = (tmp_path / "mapping.cfg")
- mapping_file.write_text(mapping_cfg)
- else:
- mapping_file = (tmp_path / "mapping.toml")
- mapping_file.write_text(mapping_toml)
- (tmp_path / "special.py").write_text("# this file is special")
- pot_path = (tmp_path / "output.pot")
- monkeypatch.chdir(tmp_path)
- cmdinst = configure_cli_command(f"extract . -o {shlex.quote(str(pot_path))} --mapping {shlex.quote(mapping_file.name)}")
- assert isinstance(cmdinst, ExtractMessages)
- cmdinst.run()
- # If the custom extractor didn't run, we wouldn't see the cookie in there.
- assert CUSTOM_EXTRACTOR_COOKIE in pot_path.read_text()
# history and logs, available at https://github.com/python-babel/babel/commits/master/.
import os
-import unittest
from io import BytesIO
from babel.messages import Catalog, mofile
from babel.support import Translations
+data_dir = os.path.join(os.path.dirname(__file__), 'data')
-class ReadMoTestCase(unittest.TestCase):
- def setUp(self):
- self.datadir = os.path.join(os.path.dirname(__file__), 'data')
+def test_basics():
+ mo_path = os.path.join(data_dir, 'project', 'i18n', 'de',
+ 'LC_MESSAGES', 'messages.mo')
+ with open(mo_path, 'rb') as mo_file:
+ catalog = mofile.read_mo(mo_file)
+ assert len(catalog) == 2
+ assert catalog.project == 'TestProject'
+ assert catalog.version == '0.1'
+ assert catalog['bar'].string == 'Stange'
+ assert catalog['foobar'].string == ['Fuhstange', 'Fuhstangen']
- def test_basics(self):
- mo_path = os.path.join(self.datadir, 'project', 'i18n', 'de',
- 'LC_MESSAGES', 'messages.mo')
- with open(mo_path, 'rb') as mo_file:
- catalog = mofile.read_mo(mo_file)
- assert len(catalog) == 2
- assert catalog.project == 'TestProject'
- assert catalog.version == '0.1'
- assert catalog['bar'].string == 'Stange'
- assert catalog['foobar'].string == ['Fuhstange', 'Fuhstangen']
-class WriteMoTestCase(unittest.TestCase):
-
- def test_sorting(self):
- # Ensure the header is sorted to the first entry so that its charset
- # can be applied to all subsequent messages by GNUTranslations
- # (ensuring all messages are safely converted to unicode)
- catalog = Catalog(locale='en_US')
- catalog.add('', '''\
+def test_sorting():
+ # Ensure the header is sorted to the first entry so that its charset
+ # can be applied to all subsequent messages by GNUTranslations
+ # (ensuring all messages are safely converted to unicode)
+ catalog = Catalog(locale='en_US')
+ catalog.add('', '''\
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n''')
- catalog.add('foo', 'Voh')
- catalog.add(('There is', 'There are'), ('Es gibt', 'Es gibt'))
- catalog.add('Fizz', '')
- catalog.add(('Fuzz', 'Fuzzes'), ('', ''))
- buf = BytesIO()
- mofile.write_mo(buf, catalog)
- buf.seek(0)
- translations = Translations(fp=buf)
- assert translations.ugettext('foo') == 'Voh'
- assert translations.ungettext('There is', 'There are', 1) == 'Es gibt'
- assert translations.ugettext('Fizz') == 'Fizz'
- assert translations.ugettext('Fuzz') == 'Fuzz'
- assert translations.ugettext('Fuzzes') == 'Fuzzes'
+ catalog.add('foo', 'Voh')
+ catalog.add(('There is', 'There are'), ('Es gibt', 'Es gibt'))
+ catalog.add('Fizz', '')
+ catalog.add(('Fuzz', 'Fuzzes'), ('', ''))
+ buf = BytesIO()
+ mofile.write_mo(buf, catalog)
+ buf.seek(0)
+ translations = Translations(fp=buf)
+ assert translations.ugettext('foo') == 'Voh'
+ assert translations.ungettext('There is', 'There are', 1) == 'Es gibt'
+ assert translations.ugettext('Fizz') == 'Fizz'
+ assert translations.ugettext('Fuzz') == 'Fuzz'
+ assert translations.ugettext('Fuzzes') == 'Fuzzes'
+
+
+def test_more_plural_forms():
+ catalog2 = Catalog(locale='ru_RU')
+ catalog2.add(('Fuzz', 'Fuzzes'), ('', '', ''))
+ buf = BytesIO()
+ mofile.write_mo(buf, catalog2)
- def test_more_plural_forms(self):
- catalog2 = Catalog(locale='ru_RU')
- catalog2.add(('Fuzz', 'Fuzzes'), ('', '', ''))
- buf = BytesIO()
- mofile.write_mo(buf, catalog2)
- def test_empty_translation_with_fallback(self):
- catalog1 = Catalog(locale='fr_FR')
- catalog1.add('', '''\
+def test_empty_translation_with_fallback():
+ catalog1 = Catalog(locale='fr_FR')
+ catalog1.add('', '''\
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n''')
- catalog1.add('Fuzz', '')
- buf1 = BytesIO()
- mofile.write_mo(buf1, catalog1)
- buf1.seek(0)
- catalog2 = Catalog(locale='fr')
- catalog2.add('', '''\
+ catalog1.add('Fuzz', '')
+ buf1 = BytesIO()
+ mofile.write_mo(buf1, catalog1)
+ buf1.seek(0)
+ catalog2 = Catalog(locale='fr')
+ catalog2.add('', '''\
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n''')
- catalog2.add('Fuzz', 'Flou')
- buf2 = BytesIO()
- mofile.write_mo(buf2, catalog2)
- buf2.seek(0)
+ catalog2.add('Fuzz', 'Flou')
+ buf2 = BytesIO()
+ mofile.write_mo(buf2, catalog2)
+ buf2.seek(0)
- translations = Translations(fp=buf1)
- translations.add_fallback(Translations(fp=buf2))
+ translations = Translations(fp=buf1)
+ translations.add_fallback(Translations(fp=buf2))
- assert translations.ugettext('Fuzz') == 'Flou'
+ assert translations.ugettext('Fuzz') == 'Flou'
# individuals. For the exact contribution history, see the revision
# history and logs, available at https://github.com/python-babel/babel/commits/master/.
-import unittest
-from datetime import datetime
from io import BytesIO, StringIO
import pytest
from babel.core import Locale
from babel.messages import pofile
-from babel.messages.catalog import Catalog, Message
+from babel.messages.catalog import Catalog
from babel.messages.pofile import _enclose_filename_if_necessary, _extract_locations
-from babel.util import FixedOffsetTimezone
-class ReadPoTestCase(unittest.TestCase):
-
- def test_preserve_locale(self):
- buf = StringIO(r'''msgid "foo"
-msgstr "Voh"''')
- catalog = pofile.read_po(buf, locale='en_US')
- assert Locale('en', 'US') == catalog.locale
-
- def test_locale_gets_overridden_by_file(self):
- buf = StringIO(r'''
-msgid ""
-msgstr ""
-"Language: en_US\n"''')
- catalog = pofile.read_po(buf, locale='de')
- assert Locale('en', 'US') == catalog.locale
- buf = StringIO(r'''
-msgid ""
-msgstr ""
-"Language: ko-KR\n"''')
- catalog = pofile.read_po(buf, locale='de')
- assert Locale('ko', 'KR') == catalog.locale
-
- def test_preserve_domain(self):
- buf = StringIO(r'''msgid "foo"
-msgstr "Voh"''')
- catalog = pofile.read_po(buf, domain='mydomain')
- assert catalog.domain == 'mydomain'
-
- def test_applies_specified_encoding_during_read(self):
- buf = BytesIO('''
-msgid ""
-msgstr ""
-"Project-Id-Version: 3.15\\n"
-"Report-Msgid-Bugs-To: Fliegender Zirkus <fliegender@zirkus.de>\\n"
-"POT-Creation-Date: 2007-09-27 11:19+0700\\n"
-"PO-Revision-Date: 2007-09-27 21:42-0700\\n"
-"Last-Translator: John <cleese@bavaria.de>\\n"
-"Language-Team: German Lang <de@babel.org>\\n"
-"Plural-Forms: nplurals=2; plural=(n != 1);\\n"
-"MIME-Version: 1.0\\n"
-"Content-Type: text/plain; charset=iso-8859-1\\n"
-"Content-Transfer-Encoding: 8bit\\n"
-"Generated-By: Babel 1.0dev-r313\\n"
-
-msgid "foo"
-msgstr "bär"'''.encode('iso-8859-1'))
- catalog = pofile.read_po(buf, locale='de_DE')
- assert catalog.get('foo').string == 'bär'
-
- def test_encoding_header_read(self):
- buf = BytesIO(b'msgid ""\nmsgstr ""\n"Content-Type: text/plain; charset=mac_roman\\n"\n')
- catalog = pofile.read_po(buf, locale='xx_XX')
- assert catalog.charset == 'mac_roman'
-
- def test_plural_forms_header_parsed(self):
- buf = BytesIO(b'msgid ""\nmsgstr ""\n"Plural-Forms: nplurals=42; plural=(n % 11);\\n"\n')
- catalog = pofile.read_po(buf, locale='xx_XX')
- assert catalog.plural_expr == '(n % 11)'
- assert catalog.num_plurals == 42
-
- def test_read_multiline(self):
- buf = StringIO(r'''msgid ""
-"Here's some text that\n"
-"includesareallylongwordthatmightbutshouldnt"
-" throw us into an infinite "
-"loop\n"
-msgstr ""''')
- catalog = pofile.read_po(buf)
- assert len(catalog) == 1
- message = list(catalog)[1]
- assert message.id == (
- "Here's some text that\nincludesareallylongwordthat"
- "mightbutshouldnt throw us into an infinite loop\n"
- )
-
- def test_fuzzy_header(self):
- buf = StringIO(r'''
-# Translations template for AReallyReallyLongNameForAProject.
-# Copyright (C) 2007 ORGANIZATION
-# This file is distributed under the same license as the
-# AReallyReallyLongNameForAProject project.
-# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
-#
-#, fuzzy
-''')
- catalog = pofile.read_po(buf)
- assert len(list(catalog)) == 1
- assert list(catalog)[0].fuzzy
-
- def test_not_fuzzy_header(self):
- buf = StringIO(r'''
-# Translations template for AReallyReallyLongNameForAProject.
-# Copyright (C) 2007 ORGANIZATION
-# This file is distributed under the same license as the
-# AReallyReallyLongNameForAProject project.
-# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
-#
-''')
- catalog = pofile.read_po(buf)
- assert len(list(catalog)) == 1
- assert not list(catalog)[0].fuzzy
-
- def test_header_entry(self):
- buf = StringIO(r'''
-# SOME DESCRIPTIVE TITLE.
-# Copyright (C) 2007 THE PACKAGE'S COPYRIGHT HOLDER
-# This file is distributed under the same license as the PACKAGE package.
-# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
-#
-#, fuzzy
-msgid ""
-msgstr ""
-"Project-Id-Version: 3.15\n"
-"Report-Msgid-Bugs-To: Fliegender Zirkus <fliegender@zirkus.de>\n"
-"POT-Creation-Date: 2007-09-27 11:19+0700\n"
-"PO-Revision-Date: 2007-09-27 21:42-0700\n"
-"Last-Translator: John <cleese@bavaria.de>\n"
-"Language: de\n"
-"Language-Team: German Lang <de@babel.org>\n"
-"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=iso-8859-2\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 1.0dev-r313\n"
-''')
- catalog = pofile.read_po(buf)
- assert len(list(catalog)) == 1
- assert catalog.version == '3.15'
- assert catalog.msgid_bugs_address == 'Fliegender Zirkus <fliegender@zirkus.de>'
- assert datetime(2007, 9, 27, 11, 19, tzinfo=FixedOffsetTimezone(7 * 60)) == catalog.creation_date
- assert catalog.last_translator == 'John <cleese@bavaria.de>'
- assert Locale('de') == catalog.locale
- assert catalog.language_team == 'German Lang <de@babel.org>'
- assert catalog.charset == 'iso-8859-2'
- assert list(catalog)[0].fuzzy
-
- def test_obsolete_message(self):
- buf = StringIO(r'''# This is an obsolete message
-#~ msgid "foo"
-#~ msgstr "Voh"
-
-# This message is not obsolete
-#: main.py:1
-msgid "bar"
-msgstr "Bahr"
-''')
- catalog = pofile.read_po(buf)
- assert len(catalog) == 1
- assert len(catalog.obsolete) == 1
- message = catalog.obsolete['foo']
- assert message.id == 'foo'
- assert message.string == 'Voh'
- assert message.user_comments == ['This is an obsolete message']
-
- def test_obsolete_message_ignored(self):
- buf = StringIO(r'''# This is an obsolete message
-#~ msgid "foo"
-#~ msgstr "Voh"
-
-# This message is not obsolete
-#: main.py:1
-msgid "bar"
-msgstr "Bahr"
-''')
- catalog = pofile.read_po(buf, ignore_obsolete=True)
- assert len(catalog) == 1
- assert len(catalog.obsolete) == 0
-
- def test_multi_line_obsolete_message(self):
- buf = StringIO(r'''# This is an obsolete message
-#~ msgid ""
-#~ "foo"
-#~ "foo"
-#~ msgstr ""
-#~ "Voh"
-#~ "Vooooh"
-
-# This message is not obsolete
-#: main.py:1
-msgid "bar"
-msgstr "Bahr"
-''')
- catalog = pofile.read_po(buf)
- assert len(catalog.obsolete) == 1
- message = catalog.obsolete['foofoo']
- assert message.id == 'foofoo'
- assert message.string == 'VohVooooh'
- assert message.user_comments == ['This is an obsolete message']
-
- def test_unit_following_multi_line_obsolete_message(self):
- buf = StringIO(r'''# This is an obsolete message
-#~ msgid ""
-#~ "foo"
-#~ "fooooooo"
-#~ msgstr ""
-#~ "Voh"
-#~ "Vooooh"
-
-# This message is not obsolete
-#: main.py:1
-msgid "bar"
-msgstr "Bahr"
-''')
- catalog = pofile.read_po(buf)
- assert len(catalog) == 1
- message = catalog['bar']
- assert message.id == 'bar'
- assert message.string == 'Bahr'
- assert message.user_comments == ['This message is not obsolete']
-
- def test_unit_before_obsolete_is_not_obsoleted(self):
- buf = StringIO(r'''
-# This message is not obsolete
-#: main.py:1
-msgid "bar"
-msgstr "Bahr"
-
-# This is an obsolete message
-#~ msgid ""
-#~ "foo"
-#~ "fooooooo"
-#~ msgstr ""
-#~ "Voh"
-#~ "Vooooh"
-''')
- catalog = pofile.read_po(buf)
- assert len(catalog) == 1
- message = catalog['bar']
- assert message.id == 'bar'
- assert message.string == 'Bahr'
- assert message.user_comments == ['This message is not obsolete']
-
- def test_with_context(self):
- buf = BytesIO(b'''# Some string in the menu
-#: main.py:1
-msgctxt "Menu"
-msgid "foo"
-msgstr "Voh"
-
-# Another string in the menu
-#: main.py:2
-msgctxt "Menu"
-msgid "bar"
-msgstr "Bahr"
-''')
- catalog = pofile.read_po(buf, ignore_obsolete=True)
- assert len(catalog) == 2
- message = catalog.get('foo', context='Menu')
- assert message.context == 'Menu'
- message = catalog.get('bar', context='Menu')
- assert message.context == 'Menu'
-
- # And verify it pass through write_po
- out_buf = BytesIO()
- pofile.write_po(out_buf, catalog, omit_header=True)
- assert out_buf.getvalue().strip() == buf.getvalue().strip()
-
- def test_obsolete_message_with_context(self):
- buf = StringIO('''
-# This message is not obsolete
-msgid "baz"
-msgstr "Bazczch"
-
-# This is an obsolete message
-#~ msgctxt "other"
-#~ msgid "foo"
-#~ msgstr "Voh"
-
-# This message is not obsolete
-#: main.py:1
-msgid "bar"
-msgstr "Bahr"
-''')
- catalog = pofile.read_po(buf)
- assert len(catalog) == 2
- assert len(catalog.obsolete) == 1
- message = catalog.obsolete[("foo", "other")]
- assert message.context == 'other'
- assert message.string == 'Voh'
-
- def test_obsolete_messages_with_context(self):
- buf = StringIO('''
-# This is an obsolete message
-#~ msgctxt "apple"
-#~ msgid "foo"
-#~ msgstr "Foo"
-
-# This is an obsolete message with the same id but different context
-#~ msgctxt "orange"
-#~ msgid "foo"
-#~ msgstr "Bar"
-''')
- catalog = pofile.read_po(buf)
- assert len(catalog) == 0
- assert len(catalog.obsolete) == 2
- assert 'foo' not in catalog.obsolete
-
- apple_msg = catalog.obsolete[('foo', 'apple')]
- assert apple_msg.id == 'foo'
- assert apple_msg.string == 'Foo'
- assert apple_msg.user_comments == ['This is an obsolete message']
-
- orange_msg = catalog.obsolete[('foo', 'orange')]
- assert orange_msg.id == 'foo'
- assert orange_msg.string == 'Bar'
- assert orange_msg.user_comments == ['This is an obsolete message with the same id but different context']
-
- def test_obsolete_messages_roundtrip(self):
- buf = StringIO('''\
-# This message is not obsolete
-#: main.py:1
-msgid "bar"
-msgstr "Bahr"
-
-# This is an obsolete message
-#~ msgid "foo"
-#~ msgstr "Voh"
-
-# This is an obsolete message
-#~ msgctxt "apple"
-#~ msgid "foo"
-#~ msgstr "Foo"
-
-# This is an obsolete message with the same id but different context
-#~ msgctxt "orange"
-#~ msgid "foo"
-#~ msgstr "Bar"
-
-''')
- generated_po_file = ''.join(pofile.generate_po(pofile.read_po(buf), omit_header=True))
- assert buf.getvalue() == generated_po_file
-
- def test_multiline_context(self):
- buf = StringIO('''
-msgctxt "a really long "
-"message context "
-"why?"
-msgid "mid"
-msgstr "mst"
- ''')
- catalog = pofile.read_po(buf)
- assert len(catalog) == 1
- message = catalog.get('mid', context="a really long message context why?")
- assert message is not None
- assert message.context == 'a really long message context why?'
-
- def test_with_context_two(self):
- buf = BytesIO(b'''msgctxt "Menu"
-msgid "foo"
-msgstr "Voh"
-
-msgctxt "Mannu"
-msgid "bar"
-msgstr "Bahr"
-''')
- catalog = pofile.read_po(buf, ignore_obsolete=True)
- assert len(catalog) == 2
- message = catalog.get('foo', context='Menu')
- assert message.context == 'Menu'
- message = catalog.get('bar', context='Mannu')
- assert message.context == 'Mannu'
-
- # And verify it pass through write_po
- out_buf = BytesIO()
- pofile.write_po(out_buf, catalog, omit_header=True)
- assert out_buf.getvalue().strip() == buf.getvalue().strip(), out_buf.getvalue()
-
- def test_single_plural_form(self):
- buf = StringIO(r'''msgid "foo"
-msgid_plural "foos"
-msgstr[0] "Voh"''')
- catalog = pofile.read_po(buf, locale='ja_JP')
- assert len(catalog) == 1
- assert catalog.num_plurals == 1
- message = catalog['foo']
- assert len(message.string) == 1
-
- def test_singular_plural_form(self):
- buf = StringIO(r'''msgid "foo"
-msgid_plural "foos"
-msgstr[0] "Voh"
-msgstr[1] "Vohs"''')
- catalog = pofile.read_po(buf, locale='nl_NL')
- assert len(catalog) == 1
- assert catalog.num_plurals == 2
- message = catalog['foo']
- assert len(message.string) == 2
-
- def test_more_than_two_plural_forms(self):
- buf = StringIO(r'''msgid "foo"
-msgid_plural "foos"
-msgstr[0] "Voh"
-msgstr[1] "Vohs"
-msgstr[2] "Vohss"''')
- catalog = pofile.read_po(buf, locale='lv_LV')
- assert len(catalog) == 1
- assert catalog.num_plurals == 3
- message = catalog['foo']
- assert len(message.string) == 3
- assert message.string[2] == 'Vohss'
-
- def test_plural_with_square_brackets(self):
- buf = StringIO(r'''msgid "foo"
-msgid_plural "foos"
-msgstr[0] "Voh [text]"
-msgstr[1] "Vohs [text]"''')
- catalog = pofile.read_po(buf, locale='nb_NO')
- assert len(catalog) == 1
- assert catalog.num_plurals == 2
- message = catalog['foo']
- assert len(message.string) == 2
-
- def test_obsolete_plural_with_square_brackets(self):
- buf = StringIO('''\
-#~ msgid "foo"
-#~ msgid_plural "foos"
-#~ msgstr[0] "Voh [text]"
-#~ msgstr[1] "Vohs [text]"
-''')
- catalog = pofile.read_po(buf, locale='nb_NO')
- assert len(catalog) == 0
- assert len(catalog.obsolete) == 1
- assert catalog.num_plurals == 2
- message = catalog.obsolete['foo']
- assert len(message.string) == 2
- assert message.string[0] == 'Voh [text]'
- assert message.string[1] == 'Vohs [text]'
-
- def test_missing_plural(self):
- buf = StringIO('''\
-msgid ""
-msgstr ""
-"Plural-Forms: nplurals=3; plural=(n < 2) ? n : 2;\n"
-
-msgid "foo"
-msgid_plural "foos"
-msgstr[0] "Voh [text]"
-msgstr[1] "Vohs [text]"
-''')
- catalog = pofile.read_po(buf, locale='nb_NO')
- assert len(catalog) == 1
- assert catalog.num_plurals == 3
- message = catalog['foo']
- assert len(message.string) == 3
- assert message.string[0] == 'Voh [text]'
- assert message.string[1] == 'Vohs [text]'
- assert message.string[2] == ''
-
- def test_missing_plural_in_the_middle(self):
- buf = StringIO('''\
-msgid ""
-msgstr ""
-"Plural-Forms: nplurals=3; plural=(n < 2) ? n : 2;\n"
-
-msgid "foo"
-msgid_plural "foos"
-msgstr[0] "Voh [text]"
-msgstr[2] "Vohs [text]"
-''')
- catalog = pofile.read_po(buf, locale='nb_NO')
- assert len(catalog) == 1
- assert catalog.num_plurals == 3
- message = catalog['foo']
- assert len(message.string) == 3
- assert message.string[0] == 'Voh [text]'
- assert message.string[1] == ''
- assert message.string[2] == 'Vohs [text]'
-
- def test_with_location(self):
- buf = StringIO('''\
-#: main.py:1 \u2068filename with whitespace.py\u2069:123
-msgid "foo"
-msgstr "bar"
-''')
- catalog = pofile.read_po(buf, locale='de_DE')
- assert len(catalog) == 1
- message = catalog['foo']
- assert message.string == 'bar'
- assert message.locations == [("main.py", 1), ("filename with whitespace.py", 123)]
-
-
- def test_abort_invalid_po_file(self):
- invalid_po = '''
- msgctxt ""
- "{\"checksum\": 2148532640, \"cxt\": \"collector_thankyou\", \"id\": "
- "270005359}"
- msgid ""
- "Thank you very much for your time.\n"
- "If you have any questions regarding this survey, please contact Fulano "
- "at nadie@blah.com"
- msgstr "Merci de prendre le temps de remplir le sondage.
- Pour toute question, veuillez communiquer avec Fulano à nadie@blah.com
- "
- '''
- invalid_po_2 = '''
- msgctxt ""
- "{\"checksum\": 2148532640, \"cxt\": \"collector_thankyou\", \"id\": "
- "270005359}"
- msgid ""
- "Thank you very much for your time.\n"
- "If you have any questions regarding this survey, please contact Fulano "
- "at fulano@blah.com."
- msgstr "Merci de prendre le temps de remplir le sondage.
- Pour toute question, veuillez communiquer avec Fulano a fulano@blah.com
- "
- '''
- # Catalog not created, throws Unicode Error
- buf = StringIO(invalid_po)
- output = pofile.read_po(buf, locale='fr', abort_invalid=False)
- assert isinstance(output, Catalog)
-
- # Catalog not created, throws PoFileError
- buf = StringIO(invalid_po_2)
- with pytest.raises(pofile.PoFileError):
- pofile.read_po(buf, locale='fr', abort_invalid=True)
-
- # Catalog is created with warning, no abort
- buf = StringIO(invalid_po_2)
- output = pofile.read_po(buf, locale='fr', abort_invalid=False)
- assert isinstance(output, Catalog)
-
- # Catalog not created, aborted with PoFileError
- buf = StringIO(invalid_po_2)
- with pytest.raises(pofile.PoFileError):
- pofile.read_po(buf, locale='fr', abort_invalid=True)
-
- def test_invalid_pofile_with_abort_flag(self):
- parser = pofile.PoFileParser(None, abort_invalid=True)
- lineno = 10
- line = 'Algo esta mal'
- msg = 'invalid file'
- with pytest.raises(pofile.PoFileError):
- parser._invalid_pofile(line, lineno, msg)
-
-
-class WritePoTestCase(unittest.TestCase):
-
- def test_join_locations(self):
- catalog = Catalog()
- catalog.add('foo', locations=[('main.py', 1)])
- catalog.add('foo', locations=[('utils.py', 3)])
- buf = BytesIO()
- pofile.write_po(buf, catalog, omit_header=True)
- assert buf.getvalue().strip() == b'''#: main.py:1 utils.py:3
-msgid "foo"
-msgstr ""'''
-
- def test_write_po_file_with_specified_charset(self):
- catalog = Catalog(charset='iso-8859-1')
- catalog.add('foo', 'äöü', locations=[('main.py', 1)])
- buf = BytesIO()
- pofile.write_po(buf, catalog, omit_header=False)
- po_file = buf.getvalue().strip()
- assert b'"Content-Type: text/plain; charset=iso-8859-1\\n"' in po_file
- assert 'msgstr "äöü"'.encode('iso-8859-1') in po_file
-
- def test_duplicate_comments(self):
- catalog = Catalog()
- catalog.add('foo', auto_comments=['A comment'])
- catalog.add('foo', auto_comments=['A comment'])
- buf = BytesIO()
- pofile.write_po(buf, catalog, omit_header=True)
- assert buf.getvalue().strip() == b'''#. A comment
-msgid "foo"
-msgstr ""'''
-
- def test_wrap_long_lines(self):
- text = """Here's some text where
-white space and line breaks matter, and should
-
-not be removed
-
-"""
- catalog = Catalog()
- catalog.add(text, locations=[('main.py', 1)])
- buf = BytesIO()
- pofile.write_po(buf, catalog, no_location=True, omit_header=True,
- width=42)
- assert buf.getvalue().strip() == b'''msgid ""
-"Here's some text where\\n"
-"white space and line breaks matter, and"
-" should\\n"
-"\\n"
-"not be removed\\n"
-"\\n"
-msgstr ""'''
-
- def test_wrap_long_lines_with_long_word(self):
- text = """Here's some text that
-includesareallylongwordthatmightbutshouldnt throw us into an infinite loop
-"""
- catalog = Catalog()
- catalog.add(text, locations=[('main.py', 1)])
- buf = BytesIO()
- pofile.write_po(buf, catalog, no_location=True, omit_header=True,
- width=32)
- assert buf.getvalue().strip() == b'''msgid ""
-"Here's some text that\\n"
-"includesareallylongwordthatmightbutshouldnt"
-" throw us into an infinite "
-"loop\\n"
-msgstr ""'''
-
- def test_wrap_long_lines_in_header(self):
- """
- Verify that long lines in the header comment are wrapped correctly.
- """
- catalog = Catalog(project='AReallyReallyLongNameForAProject',
- revision_date=datetime(2007, 4, 1))
- buf = BytesIO()
- pofile.write_po(buf, catalog)
- assert b'\n'.join(buf.getvalue().splitlines()[:7]) == b'''\
-# Translations template for AReallyReallyLongNameForAProject.
-# Copyright (C) 2007 ORGANIZATION
-# This file is distributed under the same license as the
-# AReallyReallyLongNameForAProject project.
-# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
-#
-#, fuzzy'''
-
- def test_wrap_locations_with_hyphens(self):
- catalog = Catalog()
- catalog.add('foo', locations=[
- ('doupy/templates/base/navmenu.inc.html.py', 60),
- ])
- catalog.add('foo', locations=[
- ('doupy/templates/job-offers/helpers.html', 22),
- ])
- buf = BytesIO()
- pofile.write_po(buf, catalog, omit_header=True)
- assert buf.getvalue().strip() == b'''#: doupy/templates/base/navmenu.inc.html.py:60
-#: doupy/templates/job-offers/helpers.html:22
-msgid "foo"
-msgstr ""'''
-
- def test_no_wrap_and_width_behaviour_on_comments(self):
- catalog = Catalog()
- catalog.add("Pretty dam long message id, which must really be big "
- "to test this wrap behaviour, if not it won't work.",
- locations=[("fake.py", n) for n in range(1, 30)])
- buf = BytesIO()
- pofile.write_po(buf, catalog, width=None, omit_header=True)
- assert buf.getvalue().lower() == b"""\
-#: fake.py:1 fake.py:2 fake.py:3 fake.py:4 fake.py:5 fake.py:6 fake.py:7
-#: fake.py:8 fake.py:9 fake.py:10 fake.py:11 fake.py:12 fake.py:13 fake.py:14
-#: fake.py:15 fake.py:16 fake.py:17 fake.py:18 fake.py:19 fake.py:20 fake.py:21
-#: fake.py:22 fake.py:23 fake.py:24 fake.py:25 fake.py:26 fake.py:27 fake.py:28
-#: fake.py:29
-msgid "pretty dam long message id, which must really be big to test this wrap behaviour, if not it won't work."
-msgstr ""
-
-"""
- buf = BytesIO()
- pofile.write_po(buf, catalog, width=100, omit_header=True)
- assert buf.getvalue().lower() == b"""\
-#: fake.py:1 fake.py:2 fake.py:3 fake.py:4 fake.py:5 fake.py:6 fake.py:7 fake.py:8 fake.py:9 fake.py:10
-#: fake.py:11 fake.py:12 fake.py:13 fake.py:14 fake.py:15 fake.py:16 fake.py:17 fake.py:18 fake.py:19
-#: fake.py:20 fake.py:21 fake.py:22 fake.py:23 fake.py:24 fake.py:25 fake.py:26 fake.py:27 fake.py:28
-#: fake.py:29
-msgid ""
-"pretty dam long message id, which must really be big to test this wrap behaviour, if not it won't"
-" work."
-msgstr ""
-
-"""
-
- def test_pot_with_translator_comments(self):
- catalog = Catalog()
- catalog.add('foo', locations=[('main.py', 1)],
- auto_comments=['Comment About `foo`'])
- catalog.add('bar', locations=[('utils.py', 3)],
- user_comments=['Comment About `bar` with',
- 'multiple lines.'])
- buf = BytesIO()
- pofile.write_po(buf, catalog, omit_header=True)
- assert buf.getvalue().strip() == b'''#. Comment About `foo`
-#: main.py:1
-msgid "foo"
-msgstr ""
-
-# Comment About `bar` with
-# multiple lines.
-#: utils.py:3
-msgid "bar"
-msgstr ""'''
-
- def test_po_with_obsolete_message(self):
- catalog = Catalog()
- catalog.add('foo', 'Voh', locations=[('main.py', 1)])
- catalog.obsolete['bar'] = Message('bar', 'Bahr',
- locations=[('utils.py', 3)],
- user_comments=['User comment'])
- buf = BytesIO()
- pofile.write_po(buf, catalog, omit_header=True)
- assert buf.getvalue().strip() == b'''#: main.py:1
-msgid "foo"
-msgstr "Voh"
-
-# User comment
-#~ msgid "bar"
-#~ msgstr "Bahr"'''
-
- def test_po_with_multiline_obsolete_message(self):
- catalog = Catalog()
- catalog.add('foo', 'Voh', locations=[('main.py', 1)])
- msgid = r"""Here's a message that covers
-multiple lines, and should still be handled
-correctly.
-"""
- msgstr = r"""Here's a message that covers
-multiple lines, and should still be handled
-correctly.
-"""
- catalog.obsolete[msgid] = Message(msgid, msgstr,
- locations=[('utils.py', 3)])
- buf = BytesIO()
- pofile.write_po(buf, catalog, omit_header=True)
- assert buf.getvalue().strip() == b'''#: main.py:1
-msgid "foo"
-msgstr "Voh"
-
-#~ msgid ""
-#~ "Here's a message that covers\\n"
-#~ "multiple lines, and should still be handled\\n"
-#~ "correctly.\\n"
-#~ msgstr ""
-#~ "Here's a message that covers\\n"
-#~ "multiple lines, and should still be handled\\n"
-#~ "correctly.\\n"'''
-
- def test_po_with_obsolete_message_ignored(self):
- catalog = Catalog()
- catalog.add('foo', 'Voh', locations=[('main.py', 1)])
- catalog.obsolete['bar'] = Message('bar', 'Bahr',
- locations=[('utils.py', 3)],
- user_comments=['User comment'])
- buf = BytesIO()
- pofile.write_po(buf, catalog, omit_header=True, ignore_obsolete=True)
- assert buf.getvalue().strip() == b'''#: main.py:1
-msgid "foo"
-msgstr "Voh"'''
-
- def test_po_with_previous_msgid(self):
- catalog = Catalog()
- catalog.add('foo', 'Voh', locations=[('main.py', 1)],
- previous_id='fo')
- buf = BytesIO()
- pofile.write_po(buf, catalog, omit_header=True, include_previous=True)
- assert buf.getvalue().strip() == b'''#: main.py:1
-#| msgid "fo"
-msgid "foo"
-msgstr "Voh"'''
-
- def test_po_with_previous_msgid_plural(self):
- catalog = Catalog()
- catalog.add(('foo', 'foos'), ('Voh', 'Voeh'),
- locations=[('main.py', 1)], previous_id=('fo', 'fos'))
- buf = BytesIO()
- pofile.write_po(buf, catalog, omit_header=True, include_previous=True)
- assert buf.getvalue().strip() == b'''#: main.py:1
-#| msgid "fo"
-#| msgid_plural "fos"
-msgid "foo"
-msgid_plural "foos"
-msgstr[0] "Voh"
-msgstr[1] "Voeh"'''
-
- def test_sorted_po(self):
- catalog = Catalog()
- catalog.add('bar', locations=[('utils.py', 3)],
- user_comments=['Comment About `bar` with',
- 'multiple lines.'])
- catalog.add(('foo', 'foos'), ('Voh', 'Voeh'),
- locations=[('main.py', 1)])
- buf = BytesIO()
- pofile.write_po(buf, catalog, sort_output=True)
- value = buf.getvalue().strip()
- assert b'''\
-# Comment About `bar` with
-# multiple lines.
-#: utils.py:3
-msgid "bar"
-msgstr ""
-
-#: main.py:1
-msgid "foo"
-msgid_plural "foos"
-msgstr[0] "Voh"
-msgstr[1] "Voeh"''' in value
- assert value.find(b'msgid ""') < value.find(b'msgid "bar"') < value.find(b'msgid "foo"')
-
- def test_sorted_po_context(self):
- catalog = Catalog()
- catalog.add(('foo', 'foos'), ('Voh', 'Voeh'),
- locations=[('main.py', 1)],
- context='there')
- catalog.add(('foo', 'foos'), ('Voh', 'Voeh'),
- locations=[('main.py', 1)])
- catalog.add(('foo', 'foos'), ('Voh', 'Voeh'),
- locations=[('main.py', 1)],
- context='here')
- buf = BytesIO()
- pofile.write_po(buf, catalog, sort_output=True)
- value = buf.getvalue().strip()
- # We expect the foo without ctx, followed by "here" foo and "there" foo
- assert b'''\
-#: main.py:1
-msgid "foo"
-msgid_plural "foos"
-msgstr[0] "Voh"
-msgstr[1] "Voeh"
-
-#: main.py:1
-msgctxt "here"
-msgid "foo"
-msgid_plural "foos"
-msgstr[0] "Voh"
-msgstr[1] "Voeh"
-
-#: main.py:1
-msgctxt "there"
-msgid "foo"
-msgid_plural "foos"
-msgstr[0] "Voh"
-msgstr[1] "Voeh"''' in value
-
- def test_file_sorted_po(self):
- catalog = Catalog()
- catalog.add('bar', locations=[('utils.py', 3)])
- catalog.add(('foo', 'foos'), ('Voh', 'Voeh'), locations=[('main.py', 1)])
- buf = BytesIO()
- pofile.write_po(buf, catalog, sort_by_file=True)
- value = buf.getvalue().strip()
- assert value.find(b'main.py') < value.find(b'utils.py')
-
- def test_file_with_no_lineno(self):
- catalog = Catalog()
- catalog.add('bar', locations=[('utils.py', None)],
- user_comments=['Comment About `bar` with',
- 'multiple lines.'])
- buf = BytesIO()
- pofile.write_po(buf, catalog, sort_output=True)
- value = buf.getvalue().strip()
- assert b'''\
-# Comment About `bar` with
-# multiple lines.
-#: utils.py
-msgid "bar"
-msgstr ""''' in value
-
- def test_silent_location_fallback(self):
- buf = BytesIO(b'''\
-#: broken_file.py
-msgid "missing line number"
-msgstr ""
-
-#: broken_file.py:broken_line_number
-msgid "broken line number"
-msgstr ""''')
- catalog = pofile.read_po(buf)
- assert catalog['missing line number'].locations == [('broken_file.py', None)]
- assert catalog['broken line number'].locations == []
-
- def test_include_lineno(self):
- catalog = Catalog()
- catalog.add('foo', locations=[('main.py', 1)])
- catalog.add('foo', locations=[('utils.py', 3)])
- buf = BytesIO()
- pofile.write_po(buf, catalog, omit_header=True, include_lineno=True)
- assert buf.getvalue().strip() == b'''#: main.py:1 utils.py:3
-msgid "foo"
-msgstr ""'''
-
- def test_no_include_lineno(self):
- catalog = Catalog()
- catalog.add('foo', locations=[('main.py', 1)])
- catalog.add('foo', locations=[('main.py', 2)])
- catalog.add('foo', locations=[('utils.py', 3)])
- buf = BytesIO()
- pofile.write_po(buf, catalog, omit_header=True, include_lineno=False)
- assert buf.getvalue().strip() == b'''#: main.py utils.py
-msgid "foo"
-msgstr ""'''
-
- def test_white_space_in_location(self):
- catalog = Catalog()
- catalog.add('foo', locations=[('main.py', 1)])
- catalog.add('foo', locations=[('utils b.py', 3)])
- buf = BytesIO()
- pofile.write_po(buf, catalog, omit_header=True, include_lineno=True)
- assert buf.getvalue().strip() == b'''#: main.py:1 \xe2\x81\xa8utils b.py\xe2\x81\xa9:3
-msgid "foo"
-msgstr ""'''
-
- def test_white_space_in_location_already_enclosed(self):
- catalog = Catalog()
- catalog.add('foo', locations=[('main.py', 1)])
- catalog.add('foo', locations=[('\u2068utils b.py\u2069', 3)])
- buf = BytesIO()
- pofile.write_po(buf, catalog, omit_header=True, include_lineno=True)
- assert buf.getvalue().strip() == b'''#: main.py:1 \xe2\x81\xa8utils b.py\xe2\x81\xa9:3
-msgid "foo"
-msgstr ""'''
-
- def test_tab_in_location(self):
- catalog = Catalog()
- catalog.add('foo', locations=[('main.py', 1)])
- catalog.add('foo', locations=[('utils\tb.py', 3)])
- buf = BytesIO()
- pofile.write_po(buf, catalog, omit_header=True, include_lineno=True)
- assert buf.getvalue().strip() == b'''#: main.py:1 \xe2\x81\xa8utils b.py\xe2\x81\xa9:3
-msgid "foo"
-msgstr ""'''
-
- def test_tab_in_location_already_enclosed(self):
- catalog = Catalog()
- catalog.add('foo', locations=[('main.py', 1)])
- catalog.add('foo', locations=[('\u2068utils\tb.py\u2069', 3)])
- buf = BytesIO()
- pofile.write_po(buf, catalog, omit_header=True, include_lineno=True)
- assert buf.getvalue().strip() == b'''#: main.py:1 \xe2\x81\xa8utils b.py\xe2\x81\xa9:3
-msgid "foo"
-msgstr ""'''
-
-
- def test_wrap_with_enclosed_file_locations(self):
- # Ensure that file names containing white space are not wrapped regardless of the --width parameter
- catalog = Catalog()
- catalog.add('foo', locations=[('\u2068test utils.py\u2069', 1)])
- catalog.add('foo', locations=[('\u2068test utils.py\u2069', 3)])
- buf = BytesIO()
- pofile.write_po(buf, catalog, omit_header=True, include_lineno=True, width=1)
- assert buf.getvalue().strip() == b'''#: \xe2\x81\xa8test utils.py\xe2\x81\xa9:1
-#: \xe2\x81\xa8test utils.py\xe2\x81\xa9:3
-msgid "foo"
-msgstr ""'''
-
-
-class RoundtripPoTestCase(unittest.TestCase):
+def test_enclosed_filenames_in_location_comment():
+ catalog = Catalog()
+ catalog.add("foo", lineno=2, locations=[("main 1.py", 1)], string="")
+ catalog.add("bar", lineno=6, locations=[("other.py", 2)], string="")
+ catalog.add("baz", lineno=10, locations=[("main 1.py", 3), ("other.py", 4)], string="")
+ buf = BytesIO()
+ pofile.write_po(buf, catalog, omit_header=True, include_lineno=True)
+ buf.seek(0)
+ catalog2 = pofile.read_po(buf)
+ assert True is catalog.is_identical(catalog2)
- def test_enclosed_filenames_in_location_comment(self):
- catalog = Catalog()
- catalog.add("foo", lineno=2, locations=[("main 1.py", 1)], string="")
- catalog.add("bar", lineno=6, locations=[("other.py", 2)], string="")
- catalog.add("baz", lineno=10, locations=[("main 1.py", 3), ("other.py", 4)], string="")
- buf = BytesIO()
- pofile.write_po(buf, catalog, omit_header=True, include_lineno=True)
- buf.seek(0)
- catalog2 = pofile.read_po(buf)
- assert True is catalog.is_identical(catalog2)
+def test_unescape():
+ escaped = '"Say:\\n \\"hello, world!\\"\\n"'
+ unescaped = 'Say:\n "hello, world!"\n'
+ assert unescaped != escaped
+ assert unescaped == pofile.unescape(escaped)
-class PofileFunctionsTestCase(unittest.TestCase):
- def test_unescape(self):
- escaped = '"Say:\\n \\"hello, world!\\"\\n"'
- unescaped = 'Say:\n "hello, world!"\n'
- assert unescaped != escaped
- assert unescaped == pofile.unescape(escaped)
+def test_unescape_of_quoted_newline():
+ # regression test for #198
+ assert pofile.unescape(r'"\\n"') == '\\n'
- def test_unescape_of_quoted_newline(self):
- # regression test for #198
- assert pofile.unescape(r'"\\n"') == '\\n'
- def test_denormalize_on_msgstr_without_empty_first_line(self):
- # handle irregular multi-line msgstr (no "" as first line)
- # gracefully (#171)
- msgstr = '"multi-line\\n"\n" translation"'
- expected_denormalized = 'multi-line\n translation'
+def test_denormalize_on_msgstr_without_empty_first_line():
+ # handle irregular multi-line msgstr (no "" as first line)
+ # gracefully (#171)
+ msgstr = '"multi-line\\n"\n" translation"'
+ expected_denormalized = 'multi-line\n translation'
- assert expected_denormalized == pofile.denormalize(msgstr)
- assert expected_denormalized == pofile.denormalize(f'""\n{msgstr}')
+ assert expected_denormalized == pofile.denormalize(msgstr)
+ assert expected_denormalized == pofile.denormalize(f'""\n{msgstr}')
@pytest.mark.parametrize(("line", "locations"), [
--- /dev/null
+#
+# Copyright (C) 2007-2011 Edgewall Software, 2013-2025 the Babel team
+# All rights reserved.
+#
+# This software is licensed as described in the file LICENSE, which
+# you should have received as part of this distribution. The terms
+# are also available at https://github.com/python-babel/babel/blob/master/LICENSE.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at https://github.com/python-babel/babel/commits/master/.
+
+from datetime import datetime
+from io import BytesIO, StringIO
+
+import pytest
+
+from babel import Locale
+from babel.messages import Catalog, pofile
+from babel.util import FixedOffsetTimezone
+
+
+def test_preserve_locale():
+ buf = StringIO(r'''msgid "foo"
+msgstr "Voh"''')
+ catalog = pofile.read_po(buf, locale='en_US')
+ assert Locale('en', 'US') == catalog.locale
+
+
+def test_locale_gets_overridden_by_file():
+ buf = StringIO(r'''
+msgid ""
+msgstr ""
+"Language: en_US\n"''')
+ catalog = pofile.read_po(buf, locale='de')
+ assert Locale('en', 'US') == catalog.locale
+ buf = StringIO(r'''
+msgid ""
+msgstr ""
+"Language: ko-KR\n"''')
+ catalog = pofile.read_po(buf, locale='de')
+ assert Locale('ko', 'KR') == catalog.locale
+
+
+def test_preserve_domain():
+ buf = StringIO(r'''msgid "foo"
+msgstr "Voh"''')
+ catalog = pofile.read_po(buf, domain='mydomain')
+ assert catalog.domain == 'mydomain'
+
+
+def test_applies_specified_encoding_during_read():
+ buf = BytesIO('''
+msgid ""
+msgstr ""
+"Project-Id-Version: 3.15\\n"
+"Report-Msgid-Bugs-To: Fliegender Zirkus <fliegender@zirkus.de>\\n"
+"POT-Creation-Date: 2007-09-27 11:19+0700\\n"
+"PO-Revision-Date: 2007-09-27 21:42-0700\\n"
+"Last-Translator: John <cleese@bavaria.de>\\n"
+"Language-Team: German Lang <de@babel.org>\\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\\n"
+"MIME-Version: 1.0\\n"
+"Content-Type: text/plain; charset=iso-8859-1\\n"
+"Content-Transfer-Encoding: 8bit\\n"
+"Generated-By: Babel 1.0dev-r313\\n"
+
+msgid "foo"
+msgstr "bär"'''.encode('iso-8859-1'))
+ catalog = pofile.read_po(buf, locale='de_DE')
+ assert catalog.get('foo').string == 'bär'
+
+
+def test_encoding_header_read():
+ buf = BytesIO(b'msgid ""\nmsgstr ""\n"Content-Type: text/plain; charset=mac_roman\\n"\n')
+ catalog = pofile.read_po(buf, locale='xx_XX')
+ assert catalog.charset == 'mac_roman'
+
+
+def test_plural_forms_header_parsed():
+ buf = BytesIO(b'msgid ""\nmsgstr ""\n"Plural-Forms: nplurals=42; plural=(n % 11);\\n"\n')
+ catalog = pofile.read_po(buf, locale='xx_XX')
+ assert catalog.plural_expr == '(n % 11)'
+ assert catalog.num_plurals == 42
+
+
+def test_read_multiline():
+ buf = StringIO(r'''msgid ""
+"Here's some text that\n"
+"includesareallylongwordthatmightbutshouldnt"
+" throw us into an infinite "
+"loop\n"
+msgstr ""''')
+ catalog = pofile.read_po(buf)
+ assert len(catalog) == 1
+ message = list(catalog)[1]
+ assert message.id == (
+ "Here's some text that\nincludesareallylongwordthat"
+ "mightbutshouldnt throw us into an infinite loop\n"
+ )
+
+
+def test_fuzzy_header():
+ buf = StringIO(r'''
+# Translations template for AReallyReallyLongNameForAProject.
+# Copyright (C) 2007 ORGANIZATION
+# This file is distributed under the same license as the
+# AReallyReallyLongNameForAProject project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
+#
+#, fuzzy
+''')
+ catalog = pofile.read_po(buf)
+ assert len(list(catalog)) == 1
+ assert list(catalog)[0].fuzzy
+
+
+def test_not_fuzzy_header():
+ buf = StringIO(r'''
+# Translations template for AReallyReallyLongNameForAProject.
+# Copyright (C) 2007 ORGANIZATION
+# This file is distributed under the same license as the
+# AReallyReallyLongNameForAProject project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
+#
+''')
+ catalog = pofile.read_po(buf)
+ assert len(list(catalog)) == 1
+ assert not list(catalog)[0].fuzzy
+
+
+def test_header_entry():
+ buf = StringIO(r'''
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2007 THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: 3.15\n"
+"Report-Msgid-Bugs-To: Fliegender Zirkus <fliegender@zirkus.de>\n"
+"POT-Creation-Date: 2007-09-27 11:19+0700\n"
+"PO-Revision-Date: 2007-09-27 21:42-0700\n"
+"Last-Translator: John <cleese@bavaria.de>\n"
+"Language: de\n"
+"Language-Team: German Lang <de@babel.org>\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=iso-8859-2\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 1.0dev-r313\n"
+''')
+ catalog = pofile.read_po(buf)
+ assert len(list(catalog)) == 1
+ assert catalog.version == '3.15'
+ assert catalog.msgid_bugs_address == 'Fliegender Zirkus <fliegender@zirkus.de>'
+ assert datetime(2007, 9, 27, 11, 19, tzinfo=FixedOffsetTimezone(7 * 60)) == catalog.creation_date
+ assert catalog.last_translator == 'John <cleese@bavaria.de>'
+ assert Locale('de') == catalog.locale
+ assert catalog.language_team == 'German Lang <de@babel.org>'
+ assert catalog.charset == 'iso-8859-2'
+ assert list(catalog)[0].fuzzy
+
+
+def test_obsolete_message():
+ buf = StringIO(r'''# This is an obsolete message
+#~ msgid "foo"
+#~ msgstr "Voh"
+
+# This message is not obsolete
+#: main.py:1
+msgid "bar"
+msgstr "Bahr"
+''')
+ catalog = pofile.read_po(buf)
+ assert len(catalog) == 1
+ assert len(catalog.obsolete) == 1
+ message = catalog.obsolete['foo']
+ assert message.id == 'foo'
+ assert message.string == 'Voh'
+ assert message.user_comments == ['This is an obsolete message']
+
+
+def test_obsolete_message_ignored():
+ buf = StringIO(r'''# This is an obsolete message
+#~ msgid "foo"
+#~ msgstr "Voh"
+
+# This message is not obsolete
+#: main.py:1
+msgid "bar"
+msgstr "Bahr"
+''')
+ catalog = pofile.read_po(buf, ignore_obsolete=True)
+ assert len(catalog) == 1
+ assert len(catalog.obsolete) == 0
+
+
+def test_multi_line_obsolete_message():
+ buf = StringIO(r'''# This is an obsolete message
+#~ msgid ""
+#~ "foo"
+#~ "foo"
+#~ msgstr ""
+#~ "Voh"
+#~ "Vooooh"
+
+# This message is not obsolete
+#: main.py:1
+msgid "bar"
+msgstr "Bahr"
+''')
+ catalog = pofile.read_po(buf)
+ assert len(catalog.obsolete) == 1
+ message = catalog.obsolete['foofoo']
+ assert message.id == 'foofoo'
+ assert message.string == 'VohVooooh'
+ assert message.user_comments == ['This is an obsolete message']
+
+
+def test_unit_following_multi_line_obsolete_message():
+ buf = StringIO(r'''# This is an obsolete message
+#~ msgid ""
+#~ "foo"
+#~ "fooooooo"
+#~ msgstr ""
+#~ "Voh"
+#~ "Vooooh"
+
+# This message is not obsolete
+#: main.py:1
+msgid "bar"
+msgstr "Bahr"
+''')
+ catalog = pofile.read_po(buf)
+ assert len(catalog) == 1
+ message = catalog['bar']
+ assert message.id == 'bar'
+ assert message.string == 'Bahr'
+ assert message.user_comments == ['This message is not obsolete']
+
+
+def test_unit_before_obsolete_is_not_obsoleted():
+ buf = StringIO(r'''
+# This message is not obsolete
+#: main.py:1
+msgid "bar"
+msgstr "Bahr"
+
+# This is an obsolete message
+#~ msgid ""
+#~ "foo"
+#~ "fooooooo"
+#~ msgstr ""
+#~ "Voh"
+#~ "Vooooh"
+''')
+ catalog = pofile.read_po(buf)
+ assert len(catalog) == 1
+ message = catalog['bar']
+ assert message.id == 'bar'
+ assert message.string == 'Bahr'
+ assert message.user_comments == ['This message is not obsolete']
+
+
+def test_with_context():
+ buf = BytesIO(b'''# Some string in the menu
+#: main.py:1
+msgctxt "Menu"
+msgid "foo"
+msgstr "Voh"
+
+# Another string in the menu
+#: main.py:2
+msgctxt "Menu"
+msgid "bar"
+msgstr "Bahr"
+''')
+ catalog = pofile.read_po(buf, ignore_obsolete=True)
+ assert len(catalog) == 2
+ message = catalog.get('foo', context='Menu')
+ assert message.context == 'Menu'
+ message = catalog.get('bar', context='Menu')
+ assert message.context == 'Menu'
+
+ # And verify it pass through write_po
+ out_buf = BytesIO()
+ pofile.write_po(out_buf, catalog, omit_header=True)
+ assert out_buf.getvalue().strip() == buf.getvalue().strip()
+
+
+def test_obsolete_message_with_context():
+ buf = StringIO('''
+# This message is not obsolete
+msgid "baz"
+msgstr "Bazczch"
+
+# This is an obsolete message
+#~ msgctxt "other"
+#~ msgid "foo"
+#~ msgstr "Voh"
+
+# This message is not obsolete
+#: main.py:1
+msgid "bar"
+msgstr "Bahr"
+''')
+ catalog = pofile.read_po(buf)
+ assert len(catalog) == 2
+ assert len(catalog.obsolete) == 1
+ message = catalog.obsolete[("foo", "other")]
+ assert message.context == 'other'
+ assert message.string == 'Voh'
+
+
+def test_obsolete_messages_with_context():
+ buf = StringIO('''
+# This is an obsolete message
+#~ msgctxt "apple"
+#~ msgid "foo"
+#~ msgstr "Foo"
+
+# This is an obsolete message with the same id but different context
+#~ msgctxt "orange"
+#~ msgid "foo"
+#~ msgstr "Bar"
+''')
+ catalog = pofile.read_po(buf)
+ assert len(catalog) == 0
+ assert len(catalog.obsolete) == 2
+ assert 'foo' not in catalog.obsolete
+
+ apple_msg = catalog.obsolete[('foo', 'apple')]
+ assert apple_msg.id == 'foo'
+ assert apple_msg.string == 'Foo'
+ assert apple_msg.user_comments == ['This is an obsolete message']
+
+ orange_msg = catalog.obsolete[('foo', 'orange')]
+ assert orange_msg.id == 'foo'
+ assert orange_msg.string == 'Bar'
+ assert orange_msg.user_comments == ['This is an obsolete message with the same id but different context']
+
+
+def test_obsolete_messages_roundtrip():
+ buf = StringIO('''\
+# This message is not obsolete
+#: main.py:1
+msgid "bar"
+msgstr "Bahr"
+
+# This is an obsolete message
+#~ msgid "foo"
+#~ msgstr "Voh"
+
+# This is an obsolete message
+#~ msgctxt "apple"
+#~ msgid "foo"
+#~ msgstr "Foo"
+
+# This is an obsolete message with the same id but different context
+#~ msgctxt "orange"
+#~ msgid "foo"
+#~ msgstr "Bar"
+
+''')
+ generated_po_file = ''.join(pofile.generate_po(pofile.read_po(buf), omit_header=True))
+ assert buf.getvalue() == generated_po_file
+
+
+def test_multiline_context():
+ buf = StringIO('''
+msgctxt "a really long "
+"message context "
+"why?"
+msgid "mid"
+msgstr "mst"
+ ''')
+ catalog = pofile.read_po(buf)
+ assert len(catalog) == 1
+ message = catalog.get('mid', context="a really long message context why?")
+ assert message is not None
+ assert message.context == 'a really long message context why?'
+
+
+def test_with_context_two():
+ buf = BytesIO(b'''msgctxt "Menu"
+msgid "foo"
+msgstr "Voh"
+
+msgctxt "Mannu"
+msgid "bar"
+msgstr "Bahr"
+''')
+ catalog = pofile.read_po(buf, ignore_obsolete=True)
+ assert len(catalog) == 2
+ message = catalog.get('foo', context='Menu')
+ assert message.context == 'Menu'
+ message = catalog.get('bar', context='Mannu')
+ assert message.context == 'Mannu'
+
+ # And verify it pass through write_po
+ out_buf = BytesIO()
+ pofile.write_po(out_buf, catalog, omit_header=True)
+ assert out_buf.getvalue().strip() == buf.getvalue().strip(), out_buf.getvalue()
+
+
+def test_single_plural_form():
+ buf = StringIO(r'''msgid "foo"
+msgid_plural "foos"
+msgstr[0] "Voh"''')
+ catalog = pofile.read_po(buf, locale='ja_JP')
+ assert len(catalog) == 1
+ assert catalog.num_plurals == 1
+ message = catalog['foo']
+ assert len(message.string) == 1
+
+
+def test_singular_plural_form():
+ buf = StringIO(r'''msgid "foo"
+msgid_plural "foos"
+msgstr[0] "Voh"
+msgstr[1] "Vohs"''')
+ catalog = pofile.read_po(buf, locale='nl_NL')
+ assert len(catalog) == 1
+ assert catalog.num_plurals == 2
+ message = catalog['foo']
+ assert len(message.string) == 2
+
+
+def test_more_than_two_plural_forms():
+ buf = StringIO(r'''msgid "foo"
+msgid_plural "foos"
+msgstr[0] "Voh"
+msgstr[1] "Vohs"
+msgstr[2] "Vohss"''')
+ catalog = pofile.read_po(buf, locale='lv_LV')
+ assert len(catalog) == 1
+ assert catalog.num_plurals == 3
+ message = catalog['foo']
+ assert len(message.string) == 3
+ assert message.string[2] == 'Vohss'
+
+
+def test_plural_with_square_brackets():
+ buf = StringIO(r'''msgid "foo"
+msgid_plural "foos"
+msgstr[0] "Voh [text]"
+msgstr[1] "Vohs [text]"''')
+ catalog = pofile.read_po(buf, locale='nb_NO')
+ assert len(catalog) == 1
+ assert catalog.num_plurals == 2
+ message = catalog['foo']
+ assert len(message.string) == 2
+
+
+def test_obsolete_plural_with_square_brackets():
+ buf = StringIO('''\
+#~ msgid "foo"
+#~ msgid_plural "foos"
+#~ msgstr[0] "Voh [text]"
+#~ msgstr[1] "Vohs [text]"
+''')
+ catalog = pofile.read_po(buf, locale='nb_NO')
+ assert len(catalog) == 0
+ assert len(catalog.obsolete) == 1
+ assert catalog.num_plurals == 2
+ message = catalog.obsolete['foo']
+ assert len(message.string) == 2
+ assert message.string[0] == 'Voh [text]'
+ assert message.string[1] == 'Vohs [text]'
+
+
+def test_missing_plural():
+ buf = StringIO('''\
+msgid ""
+msgstr ""
+"Plural-Forms: nplurals=3; plural=(n < 2) ? n : 2;\n"
+
+msgid "foo"
+msgid_plural "foos"
+msgstr[0] "Voh [text]"
+msgstr[1] "Vohs [text]"
+''')
+ catalog = pofile.read_po(buf, locale='nb_NO')
+ assert len(catalog) == 1
+ assert catalog.num_plurals == 3
+ message = catalog['foo']
+ assert len(message.string) == 3
+ assert message.string[0] == 'Voh [text]'
+ assert message.string[1] == 'Vohs [text]'
+ assert message.string[2] == ''
+
+
+def test_missing_plural_in_the_middle():
+ buf = StringIO('''\
+msgid ""
+msgstr ""
+"Plural-Forms: nplurals=3; plural=(n < 2) ? n : 2;\n"
+
+msgid "foo"
+msgid_plural "foos"
+msgstr[0] "Voh [text]"
+msgstr[2] "Vohs [text]"
+''')
+ catalog = pofile.read_po(buf, locale='nb_NO')
+ assert len(catalog) == 1
+ assert catalog.num_plurals == 3
+ message = catalog['foo']
+ assert len(message.string) == 3
+ assert message.string[0] == 'Voh [text]'
+ assert message.string[1] == ''
+ assert message.string[2] == 'Vohs [text]'
+
+
+def test_with_location():
+ buf = StringIO('''\
+#: main.py:1 \u2068filename with whitespace.py\u2069:123
+msgid "foo"
+msgstr "bar"
+''')
+ catalog = pofile.read_po(buf, locale='de_DE')
+ assert len(catalog) == 1
+ message = catalog['foo']
+ assert message.string == 'bar'
+ assert message.locations == [("main.py", 1), ("filename with whitespace.py", 123)]
+
+
+def test_abort_invalid_po_file():
+ invalid_po = '''
+ msgctxt ""
+ "{\"checksum\": 2148532640, \"cxt\": \"collector_thankyou\", \"id\": "
+ "270005359}"
+ msgid ""
+ "Thank you very much for your time.\n"
+ "If you have any questions regarding this survey, please contact Fulano "
+ "at nadie@blah.com"
+ msgstr "Merci de prendre le temps de remplir le sondage.
+ Pour toute question, veuillez communiquer avec Fulano à nadie@blah.com
+ "
+ '''
+ invalid_po_2 = '''
+ msgctxt ""
+ "{\"checksum\": 2148532640, \"cxt\": \"collector_thankyou\", \"id\": "
+ "270005359}"
+ msgid ""
+ "Thank you very much for your time.\n"
+ "If you have any questions regarding this survey, please contact Fulano "
+ "at fulano@blah.com."
+ msgstr "Merci de prendre le temps de remplir le sondage.
+ Pour toute question, veuillez communiquer avec Fulano a fulano@blah.com
+ "
+ '''
+ # Catalog not created, throws Unicode Error
+ buf = StringIO(invalid_po)
+ output = pofile.read_po(buf, locale='fr', abort_invalid=False)
+ assert isinstance(output, Catalog)
+
+ # Catalog not created, throws PoFileError
+ buf = StringIO(invalid_po_2)
+ with pytest.raises(pofile.PoFileError):
+ pofile.read_po(buf, locale='fr', abort_invalid=True)
+
+ # Catalog is created with warning, no abort
+ buf = StringIO(invalid_po_2)
+ output = pofile.read_po(buf, locale='fr', abort_invalid=False)
+ assert isinstance(output, Catalog)
+
+ # Catalog not created, aborted with PoFileError
+ buf = StringIO(invalid_po_2)
+ with pytest.raises(pofile.PoFileError):
+ pofile.read_po(buf, locale='fr', abort_invalid=True)
+
+
+def test_invalid_pofile_with_abort_flag():
+ parser = pofile.PoFileParser(None, abort_invalid=True)
+ lineno = 10
+ line = 'Algo esta mal'
+ msg = 'invalid file'
+ with pytest.raises(pofile.PoFileError):
+ parser._invalid_pofile(line, lineno, msg)
--- /dev/null
+#
+# Copyright (C) 2007-2011 Edgewall Software, 2013-2025 the Babel team
+# All rights reserved.
+#
+# This software is licensed as described in the file LICENSE, which
+# you should have received as part of this distribution. The terms
+# are also available at https://github.com/python-babel/babel/blob/master/LICENSE.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at https://github.com/python-babel/babel/commits/master/.
+
+from datetime import datetime
+from io import BytesIO
+
+from babel.messages import Catalog, Message, pofile
+
+
+def test_join_locations():
+ catalog = Catalog()
+ catalog.add('foo', locations=[('main.py', 1)])
+ catalog.add('foo', locations=[('utils.py', 3)])
+ buf = BytesIO()
+ pofile.write_po(buf, catalog, omit_header=True)
+ assert buf.getvalue().strip() == b'''#: main.py:1 utils.py:3
+msgid "foo"
+msgstr ""'''
+
+
+def test_write_po_file_with_specified_charset():
+ catalog = Catalog(charset='iso-8859-1')
+ catalog.add('foo', 'äöü', locations=[('main.py', 1)])
+ buf = BytesIO()
+ pofile.write_po(buf, catalog, omit_header=False)
+ po_file = buf.getvalue().strip()
+ assert b'"Content-Type: text/plain; charset=iso-8859-1\\n"' in po_file
+ assert 'msgstr "äöü"'.encode('iso-8859-1') in po_file
+
+
+def test_duplicate_comments():
+ catalog = Catalog()
+ catalog.add('foo', auto_comments=['A comment'])
+ catalog.add('foo', auto_comments=['A comment'])
+ buf = BytesIO()
+ pofile.write_po(buf, catalog, omit_header=True)
+ assert buf.getvalue().strip() == b'''#. A comment
+msgid "foo"
+msgstr ""'''
+
+
+def test_wrap_long_lines():
+ text = """Here's some text where
+white space and line breaks matter, and should
+
+not be removed
+
+"""
+ catalog = Catalog()
+ catalog.add(text, locations=[('main.py', 1)])
+ buf = BytesIO()
+ pofile.write_po(buf, catalog, no_location=True, omit_header=True,
+ width=42)
+ assert buf.getvalue().strip() == b'''msgid ""
+"Here's some text where\\n"
+"white space and line breaks matter, and"
+" should\\n"
+"\\n"
+"not be removed\\n"
+"\\n"
+msgstr ""'''
+
+
+def test_wrap_long_lines_with_long_word():
+ text = """Here's some text that
+includesareallylongwordthatmightbutshouldnt throw us into an infinite loop
+"""
+ catalog = Catalog()
+ catalog.add(text, locations=[('main.py', 1)])
+ buf = BytesIO()
+ pofile.write_po(buf, catalog, no_location=True, omit_header=True,
+ width=32)
+ assert buf.getvalue().strip() == b'''msgid ""
+"Here's some text that\\n"
+"includesareallylongwordthatmightbutshouldnt"
+" throw us into an infinite "
+"loop\\n"
+msgstr ""'''
+
+
+def test_wrap_long_lines_in_header():
+ """
+ Verify that long lines in the header comment are wrapped correctly.
+ """
+ catalog = Catalog(project='AReallyReallyLongNameForAProject',
+ revision_date=datetime(2007, 4, 1))
+ buf = BytesIO()
+ pofile.write_po(buf, catalog)
+ assert b'\n'.join(buf.getvalue().splitlines()[:7]) == b'''\
+# Translations template for AReallyReallyLongNameForAProject.
+# Copyright (C) 2007 ORGANIZATION
+# This file is distributed under the same license as the
+# AReallyReallyLongNameForAProject project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2007.
+#
+#, fuzzy'''
+
+
+def test_wrap_locations_with_hyphens():
+ catalog = Catalog()
+ catalog.add('foo', locations=[
+ ('doupy/templates/base/navmenu.inc.html.py', 60),
+ ])
+ catalog.add('foo', locations=[
+ ('doupy/templates/job-offers/helpers.html', 22),
+ ])
+ buf = BytesIO()
+ pofile.write_po(buf, catalog, omit_header=True)
+ assert buf.getvalue().strip() == b'''#: doupy/templates/base/navmenu.inc.html.py:60
+#: doupy/templates/job-offers/helpers.html:22
+msgid "foo"
+msgstr ""'''
+
+
+def test_no_wrap_and_width_behaviour_on_comments():
+ catalog = Catalog()
+ catalog.add("Pretty dam long message id, which must really be big "
+ "to test this wrap behaviour, if not it won't work.",
+ locations=[("fake.py", n) for n in range(1, 30)])
+ buf = BytesIO()
+ pofile.write_po(buf, catalog, width=None, omit_header=True)
+ assert buf.getvalue().lower() == b"""\
+#: fake.py:1 fake.py:2 fake.py:3 fake.py:4 fake.py:5 fake.py:6 fake.py:7
+#: fake.py:8 fake.py:9 fake.py:10 fake.py:11 fake.py:12 fake.py:13 fake.py:14
+#: fake.py:15 fake.py:16 fake.py:17 fake.py:18 fake.py:19 fake.py:20 fake.py:21
+#: fake.py:22 fake.py:23 fake.py:24 fake.py:25 fake.py:26 fake.py:27 fake.py:28
+#: fake.py:29
+msgid "pretty dam long message id, which must really be big to test this wrap behaviour, if not it won't work."
+msgstr ""
+
+"""
+ buf = BytesIO()
+ pofile.write_po(buf, catalog, width=100, omit_header=True)
+ assert buf.getvalue().lower() == b"""\
+#: fake.py:1 fake.py:2 fake.py:3 fake.py:4 fake.py:5 fake.py:6 fake.py:7 fake.py:8 fake.py:9 fake.py:10
+#: fake.py:11 fake.py:12 fake.py:13 fake.py:14 fake.py:15 fake.py:16 fake.py:17 fake.py:18 fake.py:19
+#: fake.py:20 fake.py:21 fake.py:22 fake.py:23 fake.py:24 fake.py:25 fake.py:26 fake.py:27 fake.py:28
+#: fake.py:29
+msgid ""
+"pretty dam long message id, which must really be big to test this wrap behaviour, if not it won't"
+" work."
+msgstr ""
+
+"""
+
+
+def test_pot_with_translator_comments():
+ catalog = Catalog()
+ catalog.add('foo', locations=[('main.py', 1)],
+ auto_comments=['Comment About `foo`'])
+ catalog.add('bar', locations=[('utils.py', 3)],
+ user_comments=['Comment About `bar` with',
+ 'multiple lines.'])
+ buf = BytesIO()
+ pofile.write_po(buf, catalog, omit_header=True)
+ assert buf.getvalue().strip() == b'''#. Comment About `foo`
+#: main.py:1
+msgid "foo"
+msgstr ""
+
+# Comment About `bar` with
+# multiple lines.
+#: utils.py:3
+msgid "bar"
+msgstr ""'''
+
+
+def test_po_with_obsolete_message():
+ catalog = Catalog()
+ catalog.add('foo', 'Voh', locations=[('main.py', 1)])
+ catalog.obsolete['bar'] = Message('bar', 'Bahr',
+ locations=[('utils.py', 3)],
+ user_comments=['User comment'])
+ buf = BytesIO()
+ pofile.write_po(buf, catalog, omit_header=True)
+ assert buf.getvalue().strip() == b'''#: main.py:1
+msgid "foo"
+msgstr "Voh"
+
+# User comment
+#~ msgid "bar"
+#~ msgstr "Bahr"'''
+
+
+def test_po_with_multiline_obsolete_message():
+ catalog = Catalog()
+ catalog.add('foo', 'Voh', locations=[('main.py', 1)])
+ msgid = r"""Here's a message that covers
+multiple lines, and should still be handled
+correctly.
+"""
+ msgstr = r"""Here's a message that covers
+multiple lines, and should still be handled
+correctly.
+"""
+ catalog.obsolete[msgid] = Message(msgid, msgstr,
+ locations=[('utils.py', 3)])
+ buf = BytesIO()
+ pofile.write_po(buf, catalog, omit_header=True)
+ assert buf.getvalue().strip() == b'''#: main.py:1
+msgid "foo"
+msgstr "Voh"
+
+#~ msgid ""
+#~ "Here's a message that covers\\n"
+#~ "multiple lines, and should still be handled\\n"
+#~ "correctly.\\n"
+#~ msgstr ""
+#~ "Here's a message that covers\\n"
+#~ "multiple lines, and should still be handled\\n"
+#~ "correctly.\\n"'''
+
+
+def test_po_with_obsolete_message_ignored():
+ catalog = Catalog()
+ catalog.add('foo', 'Voh', locations=[('main.py', 1)])
+ catalog.obsolete['bar'] = Message('bar', 'Bahr',
+ locations=[('utils.py', 3)],
+ user_comments=['User comment'])
+ buf = BytesIO()
+ pofile.write_po(buf, catalog, omit_header=True, ignore_obsolete=True)
+ assert buf.getvalue().strip() == b'''#: main.py:1
+msgid "foo"
+msgstr "Voh"'''
+
+
+def test_po_with_previous_msgid():
+ catalog = Catalog()
+ catalog.add('foo', 'Voh', locations=[('main.py', 1)],
+ previous_id='fo')
+ buf = BytesIO()
+ pofile.write_po(buf, catalog, omit_header=True, include_previous=True)
+ assert buf.getvalue().strip() == b'''#: main.py:1
+#| msgid "fo"
+msgid "foo"
+msgstr "Voh"'''
+
+
+def test_po_with_previous_msgid_plural():
+ catalog = Catalog()
+ catalog.add(('foo', 'foos'), ('Voh', 'Voeh'),
+ locations=[('main.py', 1)], previous_id=('fo', 'fos'))
+ buf = BytesIO()
+ pofile.write_po(buf, catalog, omit_header=True, include_previous=True)
+ assert buf.getvalue().strip() == b'''#: main.py:1
+#| msgid "fo"
+#| msgid_plural "fos"
+msgid "foo"
+msgid_plural "foos"
+msgstr[0] "Voh"
+msgstr[1] "Voeh"'''
+
+
+def test_sorted_po():
+ catalog = Catalog()
+ catalog.add('bar', locations=[('utils.py', 3)],
+ user_comments=['Comment About `bar` with',
+ 'multiple lines.'])
+ catalog.add(('foo', 'foos'), ('Voh', 'Voeh'),
+ locations=[('main.py', 1)])
+ buf = BytesIO()
+ pofile.write_po(buf, catalog, sort_output=True)
+ value = buf.getvalue().strip()
+ assert b'''\
+# Comment About `bar` with
+# multiple lines.
+#: utils.py:3
+msgid "bar"
+msgstr ""
+
+#: main.py:1
+msgid "foo"
+msgid_plural "foos"
+msgstr[0] "Voh"
+msgstr[1] "Voeh"''' in value
+ assert value.find(b'msgid ""') < value.find(b'msgid "bar"') < value.find(b'msgid "foo"')
+
+
+def test_sorted_po_context():
+ catalog = Catalog()
+ catalog.add(('foo', 'foos'), ('Voh', 'Voeh'),
+ locations=[('main.py', 1)],
+ context='there')
+ catalog.add(('foo', 'foos'), ('Voh', 'Voeh'),
+ locations=[('main.py', 1)])
+ catalog.add(('foo', 'foos'), ('Voh', 'Voeh'),
+ locations=[('main.py', 1)],
+ context='here')
+ buf = BytesIO()
+ pofile.write_po(buf, catalog, sort_output=True)
+ value = buf.getvalue().strip()
+ # We expect the foo without ctx, followed by "here" foo and "there" foo
+ assert b'''\
+#: main.py:1
+msgid "foo"
+msgid_plural "foos"
+msgstr[0] "Voh"
+msgstr[1] "Voeh"
+
+#: main.py:1
+msgctxt "here"
+msgid "foo"
+msgid_plural "foos"
+msgstr[0] "Voh"
+msgstr[1] "Voeh"
+
+#: main.py:1
+msgctxt "there"
+msgid "foo"
+msgid_plural "foos"
+msgstr[0] "Voh"
+msgstr[1] "Voeh"''' in value
+
+
+def test_file_sorted_po():
+ catalog = Catalog()
+ catalog.add('bar', locations=[('utils.py', 3)])
+ catalog.add(('foo', 'foos'), ('Voh', 'Voeh'), locations=[('main.py', 1)])
+ buf = BytesIO()
+ pofile.write_po(buf, catalog, sort_by_file=True)
+ value = buf.getvalue().strip()
+ assert value.find(b'main.py') < value.find(b'utils.py')
+
+
+def test_file_with_no_lineno():
+ catalog = Catalog()
+ catalog.add('bar', locations=[('utils.py', None)],
+ user_comments=['Comment About `bar` with',
+ 'multiple lines.'])
+ buf = BytesIO()
+ pofile.write_po(buf, catalog, sort_output=True)
+ value = buf.getvalue().strip()
+ assert b'''\
+# Comment About `bar` with
+# multiple lines.
+#: utils.py
+msgid "bar"
+msgstr ""''' in value
+
+
+def test_silent_location_fallback():
+ buf = BytesIO(b'''\
+#: broken_file.py
+msgid "missing line number"
+msgstr ""
+
+#: broken_file.py:broken_line_number
+msgid "broken line number"
+msgstr ""''')
+ catalog = pofile.read_po(buf)
+ assert catalog['missing line number'].locations == [('broken_file.py', None)]
+ assert catalog['broken line number'].locations == []
+
+
+def test_include_lineno():
+ catalog = Catalog()
+ catalog.add('foo', locations=[('main.py', 1)])
+ catalog.add('foo', locations=[('utils.py', 3)])
+ buf = BytesIO()
+ pofile.write_po(buf, catalog, omit_header=True, include_lineno=True)
+ assert buf.getvalue().strip() == b'''#: main.py:1 utils.py:3
+msgid "foo"
+msgstr ""'''
+
+
+def test_no_include_lineno():
+ catalog = Catalog()
+ catalog.add('foo', locations=[('main.py', 1)])
+ catalog.add('foo', locations=[('main.py', 2)])
+ catalog.add('foo', locations=[('utils.py', 3)])
+ buf = BytesIO()
+ pofile.write_po(buf, catalog, omit_header=True, include_lineno=False)
+ assert buf.getvalue().strip() == b'''#: main.py utils.py
+msgid "foo"
+msgstr ""'''
+
+
+def test_white_space_in_location():
+ catalog = Catalog()
+ catalog.add('foo', locations=[('main.py', 1)])
+ catalog.add('foo', locations=[('utils b.py', 3)])
+ buf = BytesIO()
+ pofile.write_po(buf, catalog, omit_header=True, include_lineno=True)
+ assert buf.getvalue().strip() == b'''#: main.py:1 \xe2\x81\xa8utils b.py\xe2\x81\xa9:3
+msgid "foo"
+msgstr ""'''
+
+
+def test_white_space_in_location_already_enclosed():
+ catalog = Catalog()
+ catalog.add('foo', locations=[('main.py', 1)])
+ catalog.add('foo', locations=[('\u2068utils b.py\u2069', 3)])
+ buf = BytesIO()
+ pofile.write_po(buf, catalog, omit_header=True, include_lineno=True)
+ assert buf.getvalue().strip() == b'''#: main.py:1 \xe2\x81\xa8utils b.py\xe2\x81\xa9:3
+msgid "foo"
+msgstr ""'''
+
+
+def test_tab_in_location():
+ catalog = Catalog()
+ catalog.add('foo', locations=[('main.py', 1)])
+ catalog.add('foo', locations=[('utils\tb.py', 3)])
+ buf = BytesIO()
+ pofile.write_po(buf, catalog, omit_header=True, include_lineno=True)
+ assert buf.getvalue().strip() == b'''#: main.py:1 \xe2\x81\xa8utils b.py\xe2\x81\xa9:3
+msgid "foo"
+msgstr ""'''
+
+
+def test_tab_in_location_already_enclosed():
+ catalog = Catalog()
+ catalog.add('foo', locations=[('main.py', 1)])
+ catalog.add('foo', locations=[('\u2068utils\tb.py\u2069', 3)])
+ buf = BytesIO()
+ pofile.write_po(buf, catalog, omit_header=True, include_lineno=True)
+ assert buf.getvalue().strip() == b'''#: main.py:1 \xe2\x81\xa8utils b.py\xe2\x81\xa9:3
+msgid "foo"
+msgstr ""'''
+
+
+def test_wrap_with_enclosed_file_locations():
+ # Ensure that file names containing white space are not wrapped regardless of the --width parameter
+ catalog = Catalog()
+ catalog.add('foo', locations=[('\u2068test utils.py\u2069', 1)])
+ catalog.add('foo', locations=[('\u2068test utils.py\u2069', 3)])
+ buf = BytesIO()
+ pofile.write_po(buf, catalog, omit_header=True, include_lineno=True, width=1)
+ assert buf.getvalue().strip() == b'''#: \xe2\x81\xa8test utils.py\xe2\x81\xa9:1
+#: \xe2\x81\xa8test utils.py\xe2\x81\xa9:3
+msgid "foo"
+msgstr ""'''
+from __future__ import annotations
+
CUSTOM_EXTRACTOR_COOKIE = "custom extractor was here"
if "treat" not in options:
raise RuntimeError(f"The custom extractor refuses to run without a delicious treat; got {options!r}")
return [(1, next(iter(keywords)), (CUSTOM_EXTRACTOR_COOKIE,), [])]
+
+
+class Distribution: # subset of distutils.dist.Distribution
+ def __init__(self, attrs: dict) -> None:
+ self.attrs = attrs
+
+ def get_name(self) -> str:
+ return self.attrs['name']
+
+ def get_version(self) -> str:
+ return self.attrs['version']
+
+ @property
+ def packages(self) -> list[str]:
+ return self.attrs['packages']
expected = '%04d-W%02d-%d' % value.isocalendar()
assert week_number(value) == expected
+
def test_week_numbering_monday_mindays_4():
locale = Locale.parse('de_DE')
assert locale.first_week_day == 0
import random
import sys
import tempfile
-import unittest
import pytest
from babel import Locale, UnknownLocaleError, localedata
-class MergeResolveTestCase(unittest.TestCase):
-
- def test_merge_items(self):
- d = {1: 'foo', 3: 'baz'}
- localedata.merge(d, {1: 'Foo', 2: 'Bar'})
- assert d == {1: 'Foo', 2: 'Bar', 3: 'baz'}
-
- def test_merge_nested_dict(self):
- d1 = {'x': {'a': 1, 'b': 2, 'c': 3}}
- d2 = {'x': {'a': 1, 'b': 12, 'd': 14}}
- localedata.merge(d1, d2)
- assert d1 == {'x': {'a': 1, 'b': 12, 'c': 3, 'd': 14}}
-
- def test_merge_nested_dict_no_overlap(self):
- d1 = {'x': {'a': 1, 'b': 2}}
- d2 = {'y': {'a': 11, 'b': 12}}
- localedata.merge(d1, d2)
- assert d1 == {'x': {'a': 1, 'b': 2}, 'y': {'a': 11, 'b': 12}}
-
- def test_merge_with_alias_and_resolve(self):
- alias = localedata.Alias('x')
- d1 = {
- 'x': {'a': 1, 'b': 2, 'c': 3},
- 'y': alias,
- }
- d2 = {
- 'x': {'a': 1, 'b': 12, 'd': 14},
- 'y': {'b': 22, 'e': 25},
- }
- localedata.merge(d1, d2)
- assert d1 == {'x': {'a': 1, 'b': 12, 'c': 3, 'd': 14}, 'y': (alias, {'b': 22, 'e': 25})}
- d = localedata.LocaleDataDict(d1)
- assert dict(d.items()) == {'x': {'a': 1, 'b': 12, 'c': 3, 'd': 14}, 'y': {'a': 1, 'b': 22, 'c': 3, 'd': 14, 'e': 25}}
+def test_merge_items():
+ d = {1: 'foo', 3: 'baz'}
+ localedata.merge(d, {1: 'Foo', 2: 'Bar'})
+ assert d == {1: 'Foo', 2: 'Bar', 3: 'baz'}
+
+
+def test_merge_nested_dict():
+ d1 = {'x': {'a': 1, 'b': 2, 'c': 3}}
+ d2 = {'x': {'a': 1, 'b': 12, 'd': 14}}
+ localedata.merge(d1, d2)
+ assert d1 == {'x': {'a': 1, 'b': 12, 'c': 3, 'd': 14}}
+
+
+def test_merge_nested_dict_no_overlap():
+ d1 = {'x': {'a': 1, 'b': 2}}
+ d2 = {'y': {'a': 11, 'b': 12}}
+ localedata.merge(d1, d2)
+ assert d1 == {'x': {'a': 1, 'b': 2}, 'y': {'a': 11, 'b': 12}}
+
+
+def test_merge_with_alias_and_resolve():
+ alias = localedata.Alias('x')
+ d1 = {
+ 'x': {'a': 1, 'b': 2, 'c': 3},
+ 'y': alias,
+ }
+ d2 = {
+ 'x': {'a': 1, 'b': 12, 'd': 14},
+ 'y': {'b': 22, 'e': 25},
+ }
+ localedata.merge(d1, d2)
+ assert d1 == {'x': {'a': 1, 'b': 12, 'c': 3, 'd': 14}, 'y': (alias, {'b': 22, 'e': 25})}
+ d = localedata.LocaleDataDict(d1)
+ assert dict(d.items()) == {'x': {'a': 1, 'b': 12, 'c': 3, 'd': 14}, 'y': {'a': 1, 'b': 22, 'c': 3, 'd': 14, 'e': 25}}
def test_load():
# history and logs, available at https://github.com/python-babel/babel/commits/master/.
import decimal
-import unittest
from datetime import date
import pytest
)
-class FormatDecimalTestCase(unittest.TestCase):
-
- def test_patterns(self):
- assert numbers.format_decimal(12345, '##0', locale='en_US') == '12345'
- assert numbers.format_decimal(6.5, '0.00', locale='sv') == '6,50'
- assert numbers.format_decimal((10.0 ** 20), '#.00', locale='en_US') == '100000000000000000000.00'
- # regression test for #183, fraction digits were not correctly cut
- # if the input was a float value and the value had more than 7
- # significant digits
- assert numbers.format_decimal(12345678.051, '#,##0.00', locale='en_US') == '12,345,678.05'
-
- def test_subpatterns(self):
- assert numbers.format_decimal((- 12345), '#,##0.##;-#', locale='en_US') == '-12,345'
- assert numbers.format_decimal((- 12345), '#,##0.##;(#)', locale='en_US') == '(12,345)'
-
- def test_default_rounding(self):
- """
- Testing Round-Half-Even (Banker's rounding)
-
- A '5' is rounded to the closest 'even' number
- """
- assert numbers.format_decimal(5.5, '0', locale='sv') == '6'
- assert numbers.format_decimal(6.5, '0', locale='sv') == '6'
- assert numbers.format_decimal(1.2325, locale='sv') == '1,232'
- assert numbers.format_decimal(1.2335, locale='sv') == '1,234'
-
- def test_significant_digits(self):
- """Test significant digits patterns"""
- assert numbers.format_decimal(123004, '@@', locale='en_US') == '120000'
- assert numbers.format_decimal(1.12, '@', locale='sv') == '1'
- assert numbers.format_decimal(1.1, '@@', locale='sv') == '1,1'
- assert numbers.format_decimal(1.1, '@@@@@##', locale='sv') == '1,1000'
- assert numbers.format_decimal(0.0001, '@@@', locale='sv') == '0,000100'
- assert numbers.format_decimal(0.0001234, '@@@', locale='sv') == '0,000123'
- assert numbers.format_decimal(0.0001234, '@@@#', locale='sv') == '0,0001234'
- assert numbers.format_decimal(0.0001234, '@@@#', locale='sv') == '0,0001234'
- assert numbers.format_decimal(0.12345, '@@@', locale='sv') == '0,123'
- assert numbers.format_decimal(3.14159, '@@##', locale='sv') == '3,142'
- assert numbers.format_decimal(1.23004, '@@##', locale='sv') == '1,23'
- assert numbers.format_decimal(1230.04, '@@,@@', locale='en_US') == '12,30'
- assert numbers.format_decimal(123.41, '@@##', locale='en_US') == '123.4'
- assert numbers.format_decimal(1, '@@', locale='en_US') == '1.0'
- assert numbers.format_decimal(0, '@', locale='en_US') == '0'
- assert numbers.format_decimal(0.1, '@', locale='en_US') == '0.1'
- assert numbers.format_decimal(0.1, '@#', locale='en_US') == '0.1'
- assert numbers.format_decimal(0.1, '@@', locale='en_US') == '0.10'
-
- def test_decimals(self):
- """Test significant digits patterns"""
- assert numbers.format_decimal(decimal.Decimal('1.2345'), '#.00', locale='en_US') == '1.23'
- assert numbers.format_decimal(decimal.Decimal('1.2345000'), '#.00', locale='en_US') == '1.23'
- assert numbers.format_decimal(decimal.Decimal('1.2345000'), '@@', locale='en_US') == '1.2'
- assert numbers.format_decimal(decimal.Decimal('12345678901234567890.12345'), '#.00', locale='en_US') == '12345678901234567890.12'
-
- def test_scientific_notation(self):
- assert numbers.format_scientific(0.1, '#E0', locale='en_US') == '1E-1'
- assert numbers.format_scientific(0.01, '#E0', locale='en_US') == '1E-2'
- assert numbers.format_scientific(10, '#E0', locale='en_US') == '1E1'
- assert numbers.format_scientific(1234, '0.###E0', locale='en_US') == '1.234E3'
- assert numbers.format_scientific(1234, '0.#E0', locale='en_US') == '1.2E3'
- # Exponent grouping
- assert numbers.format_scientific(12345, '##0.####E0', locale='en_US') == '1.2345E4'
- # Minimum number of int digits
- assert numbers.format_scientific(12345, '00.###E0', locale='en_US') == '12.345E3'
- assert numbers.format_scientific(-12345.6, '00.###E0', locale='en_US') == '-12.346E3'
- assert numbers.format_scientific(-0.01234, '00.###E0', locale='en_US') == '-12.34E-3'
- # Custom pattern suffix
- assert numbers.format_scientific(123.45, '#.##E0 m/s', locale='en_US') == '1.23E2 m/s'
- # Exponent patterns
- assert numbers.format_scientific(123.45, '#.##E00 m/s', locale='en_US') == '1.23E02 m/s'
- assert numbers.format_scientific(0.012345, '#.##E00 m/s', locale='en_US') == '1.23E-02 m/s'
- assert numbers.format_scientific(decimal.Decimal('12345'), '#.##E+00 m/s', locale='en_US') == '1.23E+04 m/s'
- # 0 (see ticket #99)
- assert numbers.format_scientific(0, '#E0', locale='en_US') == '0E0'
-
- def test_formatting_of_very_small_decimals(self):
- # previously formatting very small decimals could lead to a type error
- # because the Decimal->string conversion was too simple (see #214)
- number = decimal.Decimal("7E-7")
- assert numbers.format_decimal(number, format="@@@", locale='en_US') == '0.000000700'
-
- def test_nan_and_infinity(self):
- assert numbers.format_decimal(decimal.Decimal('Infinity'), locale='en_US') == '∞'
- assert numbers.format_decimal(decimal.Decimal('-Infinity'), locale='en_US') == '-∞'
- assert numbers.format_decimal(decimal.Decimal('NaN'), locale='en_US') == 'NaN'
- assert numbers.format_compact_decimal(decimal.Decimal('Infinity'), locale='en_US', format_type="short") == '∞'
- assert numbers.format_compact_decimal(decimal.Decimal('-Infinity'), locale='en_US', format_type="short") == '-∞'
- assert numbers.format_compact_decimal(decimal.Decimal('NaN'), locale='en_US', format_type="short") == 'NaN'
- assert numbers.format_currency(decimal.Decimal('Infinity'), 'USD', locale='en_US') == '$∞'
- assert numbers.format_currency(decimal.Decimal('-Infinity'), 'USD', locale='en_US') == '-$∞'
-
- def test_group_separator(self):
- assert numbers.format_decimal(29567.12, locale='en_US', group_separator=False) == '29567.12'
- assert numbers.format_decimal(29567.12, locale='fr_CA', group_separator=False) == '29567,12'
- assert numbers.format_decimal(29567.12, locale='pt_BR', group_separator=False) == '29567,12'
- assert numbers.format_currency(1099.98, 'USD', locale='en_US', group_separator=False) == '$1099.98'
- assert numbers.format_currency(101299.98, 'EUR', locale='fr_CA', group_separator=False) == '101299,98\xa0€'
- assert numbers.format_currency(101299.98, 'EUR', locale='en_US', group_separator=False, format_type='name') == '101299.98 euros'
- assert numbers.format_percent(251234.1234, locale='sv_SE', group_separator=False) == '25123412\xa0%'
-
- assert numbers.format_decimal(29567.12, locale='en_US', group_separator=True) == '29,567.12'
- assert numbers.format_decimal(29567.12, locale='fr_CA', group_separator=True) == '29\xa0567,12'
- assert numbers.format_decimal(29567.12, locale='pt_BR', group_separator=True) == '29.567,12'
- assert numbers.format_currency(1099.98, 'USD', locale='en_US', group_separator=True) == '$1,099.98'
- assert numbers.format_currency(101299.98, 'EUR', locale='fr_CA', group_separator=True) == '101\xa0299,98\xa0€'
- assert numbers.format_currency(101299.98, 'EUR', locale='en_US', group_separator=True, format_type='name') == '101,299.98 euros'
- assert numbers.format_percent(251234.1234, locale='sv_SE', group_separator=True) == '25\xa0123\xa0412\xa0%'
-
- def test_compact(self):
- assert numbers.format_compact_decimal(1, locale='en_US', format_type="short") == '1'
- assert numbers.format_compact_decimal(999, locale='en_US', format_type="short") == '999'
- assert numbers.format_compact_decimal(1000, locale='en_US', format_type="short") == '1K'
- assert numbers.format_compact_decimal(9000, locale='en_US', format_type="short") == '9K'
- assert numbers.format_compact_decimal(9123, locale='en_US', format_type="short", fraction_digits=2) == '9.12K'
- assert numbers.format_compact_decimal(10000, locale='en_US', format_type="short") == '10K'
- assert numbers.format_compact_decimal(10000, locale='en_US', format_type="short", fraction_digits=2) == '10K'
- assert numbers.format_compact_decimal(1000000, locale='en_US', format_type="short") == '1M'
- assert numbers.format_compact_decimal(9000999, locale='en_US', format_type="short") == '9M'
- assert numbers.format_compact_decimal(9000900099, locale='en_US', format_type="short", fraction_digits=5) == '9.0009B'
- assert numbers.format_compact_decimal(1, locale='en_US', format_type="long") == '1'
- assert numbers.format_compact_decimal(999, locale='en_US', format_type="long") == '999'
- assert numbers.format_compact_decimal(1000, locale='en_US', format_type="long") == '1 thousand'
- assert numbers.format_compact_decimal(9000, locale='en_US', format_type="long") == '9 thousand'
- assert numbers.format_compact_decimal(9000, locale='en_US', format_type="long", fraction_digits=2) == '9 thousand'
- assert numbers.format_compact_decimal(10000, locale='en_US', format_type="long") == '10 thousand'
- assert numbers.format_compact_decimal(10000, locale='en_US', format_type="long", fraction_digits=2) == '10 thousand'
- assert numbers.format_compact_decimal(1000000, locale='en_US', format_type="long") == '1 million'
- assert numbers.format_compact_decimal(9999999, locale='en_US', format_type="long") == '10 million'
- assert numbers.format_compact_decimal(9999999999, locale='en_US', format_type="long", fraction_digits=5) == '10 billion'
- assert numbers.format_compact_decimal(1, locale='ja_JP', format_type="short") == '1'
- assert numbers.format_compact_decimal(999, locale='ja_JP', format_type="short") == '999'
- assert numbers.format_compact_decimal(1000, locale='ja_JP', format_type="short") == '1000'
- assert numbers.format_compact_decimal(9123, locale='ja_JP', format_type="short") == '9123'
- assert numbers.format_compact_decimal(10000, locale='ja_JP', format_type="short") == '1万'
- assert numbers.format_compact_decimal(1234567, locale='ja_JP', format_type="short") == '123万'
- assert numbers.format_compact_decimal(-1, locale='en_US', format_type="short") == '-1'
- assert numbers.format_compact_decimal(-1234, locale='en_US', format_type="short", fraction_digits=2) == '-1.23K'
- assert numbers.format_compact_decimal(-123456789, format_type='short', locale='en_US') == '-123M'
- assert numbers.format_compact_decimal(-123456789, format_type='long', locale='en_US') == '-123 million'
- assert numbers.format_compact_decimal(2345678, locale='mk', format_type='long') == '2 милиони'
- assert numbers.format_compact_decimal(21000000, locale='mk', format_type='long') == '21 милион'
- assert numbers.format_compact_decimal(21345, locale="gv", format_type="short") == '21K'
- assert numbers.format_compact_decimal(1000, locale='it', format_type='long') == 'mille'
- assert numbers.format_compact_decimal(1234, locale='it', format_type='long') == '1 mila'
- assert numbers.format_compact_decimal(1000, locale='fr', format_type='long') == 'mille'
- assert numbers.format_compact_decimal(1234, locale='fr', format_type='long') == '1 millier'
- assert numbers.format_compact_decimal(
- 12345, format_type="short", locale='ar_EG', fraction_digits=2, numbering_system='default',
- ) == '12٫34\xa0ألف'
- assert numbers.format_compact_decimal(
- 12345, format_type="short", locale='ar_EG', fraction_digits=2, numbering_system='latn',
- ) == '12.34\xa0ألف'
-
-
-class NumberParsingTestCase(unittest.TestCase):
-
- def test_can_parse_decimals(self):
- assert decimal.Decimal('1099.98') == numbers.parse_decimal('1,099.98', locale='en_US')
- assert decimal.Decimal('1099.98') == numbers.parse_decimal('1.099,98', locale='de')
- assert decimal.Decimal('1099.98') == numbers.parse_decimal('1,099.98', locale='ar', numbering_system="default")
- assert decimal.Decimal('1099.98') == numbers.parse_decimal('1٬099٫98', locale='ar_EG', numbering_system="default")
- with pytest.raises(numbers.NumberFormatError):
- numbers.parse_decimal('2,109,998', locale='de')
- with pytest.raises(numbers.UnsupportedNumberingSystemError):
- numbers.parse_decimal('2,109,998', locale='de', numbering_system="unknown")
-
- def test_parse_decimal_strict_mode(self):
- # Numbers with a misplaced grouping symbol should be rejected
- with pytest.raises(numbers.NumberFormatError) as info:
- numbers.parse_decimal('11.11', locale='de', strict=True)
- assert info.value.suggestions == ['1.111', '11,11']
- # Numbers with two misplaced grouping symbols should be rejected
- with pytest.raises(numbers.NumberFormatError) as info:
- numbers.parse_decimal('80.00.00', locale='de', strict=True)
- assert info.value.suggestions == ['800.000']
- # Partially grouped numbers should be rejected
- with pytest.raises(numbers.NumberFormatError) as info:
- numbers.parse_decimal('2000,000', locale='en_US', strict=True)
- assert info.value.suggestions == ['2,000,000', '2,000']
- # Numbers with duplicate grouping symbols should be rejected
- with pytest.raises(numbers.NumberFormatError) as info:
- numbers.parse_decimal('0,,000', locale='en_US', strict=True)
- assert info.value.suggestions == ['0']
- # Return only suggestion for 0 on strict
- with pytest.raises(numbers.NumberFormatError) as info:
- numbers.parse_decimal('0.00', locale='de', strict=True)
- assert info.value.suggestions == ['0']
- # Properly formatted numbers should be accepted
- assert str(numbers.parse_decimal('1.001', locale='de', strict=True)) == '1001'
- # Trailing zeroes should be accepted
- assert str(numbers.parse_decimal('3.00', locale='en_US', strict=True)) == '3.00'
- # Numbers with a grouping symbol and no trailing zeroes should be accepted
- assert str(numbers.parse_decimal('3,400.6', locale='en_US', strict=True)) == '3400.6'
- # Numbers with a grouping symbol and trailing zeroes (not all zeroes after decimal) should be accepted
- assert str(numbers.parse_decimal('3,400.60', locale='en_US', strict=True)) == '3400.60'
- # Numbers with a grouping symbol and trailing zeroes (all zeroes after decimal) should be accepted
- assert str(numbers.parse_decimal('3,400.00', locale='en_US', strict=True)) == '3400.00'
- assert str(numbers.parse_decimal('3,400.0000', locale='en_US', strict=True)) == '3400.0000'
- # Numbers with a grouping symbol and no decimal part should be accepted
- assert str(numbers.parse_decimal('3,800', locale='en_US', strict=True)) == '3800'
- # Numbers without any grouping symbol should be accepted
- assert str(numbers.parse_decimal('2000.1', locale='en_US', strict=True)) == '2000.1'
- # Numbers without any grouping symbol and no decimal should be accepted
- assert str(numbers.parse_decimal('2580', locale='en_US', strict=True)) == '2580'
- # High precision numbers should be accepted
- assert str(numbers.parse_decimal('5,000001', locale='fr', strict=True)) == '5.000001'
-
-
def test_list_currencies():
assert isinstance(list_currencies(), set)
assert list_currencies().issuperset(['BAD', 'BAM', 'KRO'])
--- /dev/null
+#
+# Copyright (C) 2007-2011 Edgewall Software, 2013-2025 the Babel team
+# All rights reserved.
+#
+# This software is licensed as described in the file LICENSE, which
+# you should have received as part of this distribution. The terms
+# are also available at https://github.com/python-babel/babel/blob/master/LICENSE.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at https://github.com/python-babel/babel/commits/master/.
+
+
+import decimal
+
+from babel import numbers
+
+
+def test_patterns():
+ assert numbers.format_decimal(12345, '##0', locale='en_US') == '12345'
+ assert numbers.format_decimal(6.5, '0.00', locale='sv') == '6,50'
+ assert numbers.format_decimal((10.0 ** 20), '#.00', locale='en_US') == '100000000000000000000.00'
+ # regression test for #183, fraction digits were not correctly cut
+ # if the input was a float value and the value had more than 7
+ # significant digits
+ assert numbers.format_decimal(12345678.051, '#,##0.00', locale='en_US') == '12,345,678.05'
+
+
+def test_subpatterns():
+ assert numbers.format_decimal((- 12345), '#,##0.##;-#', locale='en_US') == '-12,345'
+ assert numbers.format_decimal((- 12345), '#,##0.##;(#)', locale='en_US') == '(12,345)'
+
+
+def test_default_rounding():
+ """
+ Testing Round-Half-Even (Banker's rounding)
+
+ A '5' is rounded to the closest 'even' number
+ """
+ assert numbers.format_decimal(5.5, '0', locale='sv') == '6'
+ assert numbers.format_decimal(6.5, '0', locale='sv') == '6'
+ assert numbers.format_decimal(1.2325, locale='sv') == '1,232'
+ assert numbers.format_decimal(1.2335, locale='sv') == '1,234'
+
+
+def test_significant_digits():
+ """Test significant digits patterns"""
+ assert numbers.format_decimal(123004, '@@', locale='en_US') == '120000'
+ assert numbers.format_decimal(1.12, '@', locale='sv') == '1'
+ assert numbers.format_decimal(1.1, '@@', locale='sv') == '1,1'
+ assert numbers.format_decimal(1.1, '@@@@@##', locale='sv') == '1,1000'
+ assert numbers.format_decimal(0.0001, '@@@', locale='sv') == '0,000100'
+ assert numbers.format_decimal(0.0001234, '@@@', locale='sv') == '0,000123'
+ assert numbers.format_decimal(0.0001234, '@@@#', locale='sv') == '0,0001234'
+ assert numbers.format_decimal(0.0001234, '@@@#', locale='sv') == '0,0001234'
+ assert numbers.format_decimal(0.12345, '@@@', locale='sv') == '0,123'
+ assert numbers.format_decimal(3.14159, '@@##', locale='sv') == '3,142'
+ assert numbers.format_decimal(1.23004, '@@##', locale='sv') == '1,23'
+ assert numbers.format_decimal(1230.04, '@@,@@', locale='en_US') == '12,30'
+ assert numbers.format_decimal(123.41, '@@##', locale='en_US') == '123.4'
+ assert numbers.format_decimal(1, '@@', locale='en_US') == '1.0'
+ assert numbers.format_decimal(0, '@', locale='en_US') == '0'
+ assert numbers.format_decimal(0.1, '@', locale='en_US') == '0.1'
+ assert numbers.format_decimal(0.1, '@#', locale='en_US') == '0.1'
+ assert numbers.format_decimal(0.1, '@@', locale='en_US') == '0.10'
+
+
+def test_decimals():
+ """Test significant digits patterns"""
+ assert numbers.format_decimal(decimal.Decimal('1.2345'), '#.00', locale='en_US') == '1.23'
+ assert numbers.format_decimal(decimal.Decimal('1.2345000'), '#.00', locale='en_US') == '1.23'
+ assert numbers.format_decimal(decimal.Decimal('1.2345000'), '@@', locale='en_US') == '1.2'
+ assert numbers.format_decimal(decimal.Decimal('12345678901234567890.12345'), '#.00', locale='en_US') == '12345678901234567890.12'
+
+
+def test_scientific_notation():
+ assert numbers.format_scientific(0.1, '#E0', locale='en_US') == '1E-1'
+ assert numbers.format_scientific(0.01, '#E0', locale='en_US') == '1E-2'
+ assert numbers.format_scientific(10, '#E0', locale='en_US') == '1E1'
+ assert numbers.format_scientific(1234, '0.###E0', locale='en_US') == '1.234E3'
+ assert numbers.format_scientific(1234, '0.#E0', locale='en_US') == '1.2E3'
+ # Exponent grouping
+ assert numbers.format_scientific(12345, '##0.####E0', locale='en_US') == '1.2345E4'
+ # Minimum number of int digits
+ assert numbers.format_scientific(12345, '00.###E0', locale='en_US') == '12.345E3'
+ assert numbers.format_scientific(-12345.6, '00.###E0', locale='en_US') == '-12.346E3'
+ assert numbers.format_scientific(-0.01234, '00.###E0', locale='en_US') == '-12.34E-3'
+ # Custom pattern suffix
+ assert numbers.format_scientific(123.45, '#.##E0 m/s', locale='en_US') == '1.23E2 m/s'
+ # Exponent patterns
+ assert numbers.format_scientific(123.45, '#.##E00 m/s', locale='en_US') == '1.23E02 m/s'
+ assert numbers.format_scientific(0.012345, '#.##E00 m/s', locale='en_US') == '1.23E-02 m/s'
+ assert numbers.format_scientific(decimal.Decimal('12345'), '#.##E+00 m/s', locale='en_US') == '1.23E+04 m/s'
+ # 0 (see ticket #99)
+ assert numbers.format_scientific(0, '#E0', locale='en_US') == '0E0'
+
+
+def test_formatting_of_very_small_decimals():
+ # previously formatting very small decimals could lead to a type error
+ # because the Decimal->string conversion was too simple (see #214)
+ number = decimal.Decimal("7E-7")
+ assert numbers.format_decimal(number, format="@@@", locale='en_US') == '0.000000700'
+
+
+def test_nan_and_infinity():
+ assert numbers.format_decimal(decimal.Decimal('Infinity'), locale='en_US') == '∞'
+ assert numbers.format_decimal(decimal.Decimal('-Infinity'), locale='en_US') == '-∞'
+ assert numbers.format_decimal(decimal.Decimal('NaN'), locale='en_US') == 'NaN'
+ assert numbers.format_compact_decimal(decimal.Decimal('Infinity'), locale='en_US', format_type="short") == '∞'
+ assert numbers.format_compact_decimal(decimal.Decimal('-Infinity'), locale='en_US', format_type="short") == '-∞'
+ assert numbers.format_compact_decimal(decimal.Decimal('NaN'), locale='en_US', format_type="short") == 'NaN'
+ assert numbers.format_currency(decimal.Decimal('Infinity'), 'USD', locale='en_US') == '$∞'
+ assert numbers.format_currency(decimal.Decimal('-Infinity'), 'USD', locale='en_US') == '-$∞'
+
+
+def test_group_separator():
+ assert numbers.format_decimal(29567.12, locale='en_US', group_separator=False) == '29567.12'
+ assert numbers.format_decimal(29567.12, locale='fr_CA', group_separator=False) == '29567,12'
+ assert numbers.format_decimal(29567.12, locale='pt_BR', group_separator=False) == '29567,12'
+ assert numbers.format_currency(1099.98, 'USD', locale='en_US', group_separator=False) == '$1099.98'
+ assert numbers.format_currency(101299.98, 'EUR', locale='fr_CA', group_separator=False) == '101299,98\xa0€'
+ assert numbers.format_currency(101299.98, 'EUR', locale='en_US', group_separator=False, format_type='name') == '101299.98 euros'
+ assert numbers.format_percent(251234.1234, locale='sv_SE', group_separator=False) == '25123412\xa0%'
+
+ assert numbers.format_decimal(29567.12, locale='en_US', group_separator=True) == '29,567.12'
+ assert numbers.format_decimal(29567.12, locale='fr_CA', group_separator=True) == '29\xa0567,12'
+ assert numbers.format_decimal(29567.12, locale='pt_BR', group_separator=True) == '29.567,12'
+ assert numbers.format_currency(1099.98, 'USD', locale='en_US', group_separator=True) == '$1,099.98'
+ assert numbers.format_currency(101299.98, 'EUR', locale='fr_CA', group_separator=True) == '101\xa0299,98\xa0€'
+ assert numbers.format_currency(101299.98, 'EUR', locale='en_US', group_separator=True, format_type='name') == '101,299.98 euros'
+ assert numbers.format_percent(251234.1234, locale='sv_SE', group_separator=True) == '25\xa0123\xa0412\xa0%'
+
+
+def test_compact():
+ assert numbers.format_compact_decimal(1, locale='en_US', format_type="short") == '1'
+ assert numbers.format_compact_decimal(999, locale='en_US', format_type="short") == '999'
+ assert numbers.format_compact_decimal(1000, locale='en_US', format_type="short") == '1K'
+ assert numbers.format_compact_decimal(9000, locale='en_US', format_type="short") == '9K'
+ assert numbers.format_compact_decimal(9123, locale='en_US', format_type="short", fraction_digits=2) == '9.12K'
+ assert numbers.format_compact_decimal(10000, locale='en_US', format_type="short") == '10K'
+ assert numbers.format_compact_decimal(10000, locale='en_US', format_type="short", fraction_digits=2) == '10K'
+ assert numbers.format_compact_decimal(1000000, locale='en_US', format_type="short") == '1M'
+ assert numbers.format_compact_decimal(9000999, locale='en_US', format_type="short") == '9M'
+ assert numbers.format_compact_decimal(9000900099, locale='en_US', format_type="short", fraction_digits=5) == '9.0009B'
+ assert numbers.format_compact_decimal(1, locale='en_US', format_type="long") == '1'
+ assert numbers.format_compact_decimal(999, locale='en_US', format_type="long") == '999'
+ assert numbers.format_compact_decimal(1000, locale='en_US', format_type="long") == '1 thousand'
+ assert numbers.format_compact_decimal(9000, locale='en_US', format_type="long") == '9 thousand'
+ assert numbers.format_compact_decimal(9000, locale='en_US', format_type="long", fraction_digits=2) == '9 thousand'
+ assert numbers.format_compact_decimal(10000, locale='en_US', format_type="long") == '10 thousand'
+ assert numbers.format_compact_decimal(10000, locale='en_US', format_type="long", fraction_digits=2) == '10 thousand'
+ assert numbers.format_compact_decimal(1000000, locale='en_US', format_type="long") == '1 million'
+ assert numbers.format_compact_decimal(9999999, locale='en_US', format_type="long") == '10 million'
+ assert numbers.format_compact_decimal(9999999999, locale='en_US', format_type="long", fraction_digits=5) == '10 billion'
+ assert numbers.format_compact_decimal(1, locale='ja_JP', format_type="short") == '1'
+ assert numbers.format_compact_decimal(999, locale='ja_JP', format_type="short") == '999'
+ assert numbers.format_compact_decimal(1000, locale='ja_JP', format_type="short") == '1000'
+ assert numbers.format_compact_decimal(9123, locale='ja_JP', format_type="short") == '9123'
+ assert numbers.format_compact_decimal(10000, locale='ja_JP', format_type="short") == '1万'
+ assert numbers.format_compact_decimal(1234567, locale='ja_JP', format_type="short") == '123万'
+ assert numbers.format_compact_decimal(-1, locale='en_US', format_type="short") == '-1'
+ assert numbers.format_compact_decimal(-1234, locale='en_US', format_type="short", fraction_digits=2) == '-1.23K'
+ assert numbers.format_compact_decimal(-123456789, format_type='short', locale='en_US') == '-123M'
+ assert numbers.format_compact_decimal(-123456789, format_type='long', locale='en_US') == '-123 million'
+ assert numbers.format_compact_decimal(2345678, locale='mk', format_type='long') == '2 милиони'
+ assert numbers.format_compact_decimal(21000000, locale='mk', format_type='long') == '21 милион'
+ assert numbers.format_compact_decimal(21345, locale="gv", format_type="short") == '21K'
+ assert numbers.format_compact_decimal(1000, locale='it', format_type='long') == 'mille'
+ assert numbers.format_compact_decimal(1234, locale='it', format_type='long') == '1 mila'
+ assert numbers.format_compact_decimal(1000, locale='fr', format_type='long') == 'mille'
+ assert numbers.format_compact_decimal(1234, locale='fr', format_type='long') == '1 millier'
+ assert numbers.format_compact_decimal(
+ 12345, format_type="short", locale='ar_EG', fraction_digits=2, numbering_system='default',
+ ) == '12٫34\xa0ألف'
+ assert numbers.format_compact_decimal(
+ 12345, format_type="short", locale='ar_EG', fraction_digits=2, numbering_system='latn',
+ ) == '12.34\xa0ألف'
--- /dev/null
+#
+# Copyright (C) 2007-2011 Edgewall Software, 2013-2025 the Babel team
+# All rights reserved.
+#
+# This software is licensed as described in the file LICENSE, which
+# you should have received as part of this distribution. The terms
+# are also available at https://github.com/python-babel/babel/blob/master/LICENSE.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at https://github.com/python-babel/babel/commits/master/.
+
+import decimal
+
+import pytest
+
+from babel import numbers
+
+
+def test_can_parse_decimals():
+ assert decimal.Decimal('1099.98') == numbers.parse_decimal('1,099.98', locale='en_US')
+ assert decimal.Decimal('1099.98') == numbers.parse_decimal('1.099,98', locale='de')
+ assert decimal.Decimal('1099.98') == numbers.parse_decimal('1,099.98', locale='ar', numbering_system="default")
+ assert decimal.Decimal('1099.98') == numbers.parse_decimal('1٬099٫98', locale='ar_EG', numbering_system="default")
+ with pytest.raises(numbers.NumberFormatError):
+ numbers.parse_decimal('2,109,998', locale='de')
+ with pytest.raises(numbers.UnsupportedNumberingSystemError):
+ numbers.parse_decimal('2,109,998', locale='de', numbering_system="unknown")
+
+
+def test_parse_decimal_strict_mode():
+ # Numbers with a misplaced grouping symbol should be rejected
+ with pytest.raises(numbers.NumberFormatError) as info:
+ numbers.parse_decimal('11.11', locale='de', strict=True)
+ assert info.value.suggestions == ['1.111', '11,11']
+ # Numbers with two misplaced grouping symbols should be rejected
+ with pytest.raises(numbers.NumberFormatError) as info:
+ numbers.parse_decimal('80.00.00', locale='de', strict=True)
+ assert info.value.suggestions == ['800.000']
+ # Partially grouped numbers should be rejected
+ with pytest.raises(numbers.NumberFormatError) as info:
+ numbers.parse_decimal('2000,000', locale='en_US', strict=True)
+ assert info.value.suggestions == ['2,000,000', '2,000']
+ # Numbers with duplicate grouping symbols should be rejected
+ with pytest.raises(numbers.NumberFormatError) as info:
+ numbers.parse_decimal('0,,000', locale='en_US', strict=True)
+ assert info.value.suggestions == ['0']
+ # Return only suggestion for 0 on strict
+ with pytest.raises(numbers.NumberFormatError) as info:
+ numbers.parse_decimal('0.00', locale='de', strict=True)
+ assert info.value.suggestions == ['0']
+ # Properly formatted numbers should be accepted
+ assert str(numbers.parse_decimal('1.001', locale='de', strict=True)) == '1001'
+ # Trailing zeroes should be accepted
+ assert str(numbers.parse_decimal('3.00', locale='en_US', strict=True)) == '3.00'
+ # Numbers with a grouping symbol and no trailing zeroes should be accepted
+ assert str(numbers.parse_decimal('3,400.6', locale='en_US', strict=True)) == '3400.6'
+ # Numbers with a grouping symbol and trailing zeroes (not all zeroes after decimal) should be accepted
+ assert str(numbers.parse_decimal('3,400.60', locale='en_US', strict=True)) == '3400.60'
+ # Numbers with a grouping symbol and trailing zeroes (all zeroes after decimal) should be accepted
+ assert str(numbers.parse_decimal('3,400.00', locale='en_US', strict=True)) == '3400.00'
+ assert str(numbers.parse_decimal('3,400.0000', locale='en_US', strict=True)) == '3400.0000'
+ # Numbers with a grouping symbol and no decimal part should be accepted
+ assert str(numbers.parse_decimal('3,800', locale='en_US', strict=True)) == '3800'
+ # Numbers without any grouping symbol should be accepted
+ assert str(numbers.parse_decimal('2000.1', locale='en_US', strict=True)) == '2000.1'
+ # Numbers without any grouping symbol and no decimal should be accepted
+ assert str(numbers.parse_decimal('2580', locale='en_US', strict=True)) == '2580'
+ # High precision numbers should be accepted
+ assert str(numbers.parse_decimal('5,000001', locale='fr', strict=True)) == '5.000001'
# individuals. For the exact contribution history, see the revision
# history and logs, available at https://github.com/python-babel/babel/commits/master/.
import decimal
-import unittest
import pytest
plural.tokenize_rule(rule_text)
-class TestNextTokenTestCase(unittest.TestCase):
+def test_next_token_empty():
+ assert not plural.test_next_token([], '')
- def test_empty(self):
- assert not plural.test_next_token([], '')
- def test_type_ok_and_no_value(self):
- assert plural.test_next_token([('word', 'and')], 'word')
+def test_next_token_type_ok_and_no_value():
+ assert plural.test_next_token([('word', 'and')], 'word')
- def test_type_ok_and_not_value(self):
- assert not plural.test_next_token([('word', 'and')], 'word', 'or')
- def test_type_ok_and_value_ok(self):
- assert plural.test_next_token([('word', 'and')], 'word', 'and')
+def test_next_token_type_ok_and_not_value():
+ assert not plural.test_next_token([('word', 'and')], 'word', 'or')
- def test_type_not_ok_and_value_ok(self):
- assert not plural.test_next_token([('abc', 'and')], 'word', 'and')
+def test_next_token_type_ok_and_value_ok():
+ assert plural.test_next_token([('word', 'and')], 'word', 'and')
-def make_range_list(*values):
- ranges = []
- for v in values:
- if isinstance(v, int):
- val_node = plural.value_node(v)
- ranges.append((val_node, val_node))
- else:
- assert isinstance(v, tuple)
- ranges.append((plural.value_node(v[0]),
- plural.value_node(v[1])))
- return plural.range_list_node(ranges)
-
-class PluralRuleParserTestCase(unittest.TestCase):
-
- def setUp(self):
- self.n = plural.ident_node('n')
-
- def n_eq(self, v):
- return 'relation', ('in', self.n, make_range_list(v))
-
- def test_error_when_unexpected_end(self):
- with pytest.raises(plural.RuleError):
- plural._Parser('n =')
-
- def test_eq_relation(self):
- assert plural._Parser('n = 1').ast == self.n_eq(1)
-
- def test_in_range_relation(self):
- assert plural._Parser('n = 2..4').ast == \
- ('relation', ('in', self.n, make_range_list((2, 4))))
-
- def test_negate(self):
- assert plural._Parser('n != 1').ast == plural.negate(self.n_eq(1))
-
- def test_or(self):
- assert plural._Parser('n = 1 or n = 2').ast ==\
- ('or', (self.n_eq(1), self.n_eq(2)))
-
- def test_and(self):
- assert plural._Parser('n = 1 and n = 2').ast ==\
- ('and', (self.n_eq(1), self.n_eq(2)))
-
- def test_or_and(self):
- assert plural._Parser('n = 0 or n != 1 and n % 100 = 1..19').ast == \
- ('or', (self.n_eq(0),
- ('and', (plural.negate(self.n_eq(1)),
- ('relation', ('in',
- ('mod', (self.n,
- plural.value_node(100))),
- (make_range_list((1, 19))))))),
- ))
+def test_next_token_type_not_ok_and_value_ok():
+ assert not plural.test_next_token([('abc', 'and')], 'word', 'and')
EXTRACT_OPERANDS_TESTS = (
--- /dev/null
+#
+# Copyright (C) 2007-2011 Edgewall Software, 2013-2025 the Babel team
+# All rights reserved.
+#
+# This software is licensed as described in the file LICENSE, which
+# you should have received as part of this distribution. The terms
+# are also available at https://github.com/python-babel/babel/blob/master/LICENSE.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at https://github.com/python-babel/babel/commits/master/.
+
+import pytest
+
+from babel import plural
+
+N_NODE = plural.ident_node('n')
+
+
+def make_range_list(*values):
+ ranges = []
+ for v in values:
+ if isinstance(v, int):
+ val_node = plural.value_node(v)
+ ranges.append((val_node, val_node))
+ else:
+ assert isinstance(v, tuple)
+ ranges.append((plural.value_node(v[0]), plural.value_node(v[1])))
+ return plural.range_list_node(ranges)
+
+
+def n_eq(v):
+ return 'relation', ('in', N_NODE, make_range_list(v))
+
+
+def test_error_when_unexpected_end():
+ with pytest.raises(plural.RuleError):
+ plural._Parser('n =')
+
+
+def test_eq_relation():
+ assert plural._Parser('n = 1').ast == n_eq(1)
+
+
+def test_in_range_relation():
+ assert plural._Parser('n = 2..4').ast == (
+ 'relation',
+ ('in', N_NODE, make_range_list((2, 4))),
+ )
+
+
+def test_negate():
+ assert plural._Parser('n != 1').ast == plural.negate(n_eq(1))
+
+
+def test_or():
+ assert plural._Parser('n = 1 or n = 2').ast == ('or', (n_eq(1), n_eq(2)))
+
+
+def test_and():
+ assert plural._Parser('n = 1 and n = 2').ast == ('and', (n_eq(1), n_eq(2)))
+
+
+def test_or_and():
+ assert plural._Parser('n = 0 or n != 1 and n % 100 = 1..19').ast == (
+ 'or',
+ (
+ n_eq(0),
+ (
+ 'and',
+ (
+ plural.negate(n_eq(1)),
+ (
+ 'relation',
+ (
+ 'in',
+ ('mod', (N_NODE, plural.value_node(100))),
+ (make_range_list((1, 19))),
+ ),
+ ),
+ ),
+ ),
+ ),
+ )
import __future__
-import unittest
from io import BytesIO
import pytest
assert not util.pathmatch('./foo/**.py', 'blah/foo/bar/baz.py')
-class FixedOffsetTimezoneTestCase(unittest.TestCase):
+def test_fixed_zone_negative_offset():
+ assert util.FixedOffsetTimezone(-60).zone == 'Etc/GMT-60'
- def test_zone_negative_offset(self):
- assert util.FixedOffsetTimezone(-60).zone == 'Etc/GMT-60'
- def test_zone_zero_offset(self):
- assert util.FixedOffsetTimezone(0).zone == 'Etc/GMT+0'
+def test_fixed_zone_zero_offset():
+ assert util.FixedOffsetTimezone(0).zone == 'Etc/GMT+0'
- def test_zone_positive_offset(self):
- assert util.FixedOffsetTimezone(330).zone == 'Etc/GMT+330'
+
+def test_fixed_zone_positive_offset():
+ assert util.FixedOffsetTimezone(330).zone == 'Etc/GMT+330'
def parse_encoding(s):