]> git.ipfire.org Git - thirdparty/babel.git/commitdiff
Small test cleanup (#1172)
authorAarni Koskela <akx@iki.fi>
Wed, 15 Jan 2025 13:01:58 +0000 (15:01 +0200)
committerGitHub <noreply@github.com>
Wed, 15 Jan 2025 13:01:58 +0000 (13:01 +0000)
* Configure Ruff formatter to preserve quotes for now
* Split support functionality tests to a separate modules
* Use standard `monkeypatch` fixture for `os.environ` patching
* Use even more `monkeypatch` for patching

---------

Co-authored-by: Tomas R. <tomas.roun8@gmail.com>
pyproject.toml
tests/conftest.py
tests/messages/test_setuptools_frontend.py
tests/test_core.py
tests/test_support.py [deleted file]
tests/test_support_format.py [new file with mode: 0644]
tests/test_support_lazy_proxy.py [new file with mode: 0644]
tests/test_support_translations.py [new file with mode: 0644]

index 2e23f7a6d73a18130603042ff424eaade2886f92..e68b6d5d1aa31b4986918bd2b71f3dede72cf531 100644 (file)
@@ -4,6 +4,9 @@ extend-exclude = [
     "tests/messages/data",
 ]
 
+[tool.ruff.format]
+quote-style = "preserve"
+
 [tool.ruff.lint]
 select = [
     "B",
index 67e3ce921ed895532957176be102e5b8b17eedd5..dab67a9a3886abbd009b16531723e8be05714b24 100644 (file)
@@ -1,5 +1,3 @@
-import os
-
 import pytest
 
 try:
@@ -16,13 +14,6 @@ except ModuleNotFoundError:
     pytz = None
 
 
-@pytest.fixture
-def os_environ(monkeypatch):
-    mock_environ = dict(os.environ)
-    monkeypatch.setattr(os, 'environ', mock_environ)
-    return mock_environ
-
-
 def pytest_generate_tests(metafunc):
     if hasattr(metafunc.function, "pytestmark"):
         for mark in metafunc.function.pytestmark:
index f3686a8b702013c81510c08442028a9f33c57a55..a623efd29985cc2b6b1b3d8aeecf7117943e40fa 100644 (file)
@@ -56,12 +56,11 @@ def test_setuptools_commands(tmp_path, monkeypatch):
     shutil.copytree(data_dir, dest)
     monkeypatch.chdir(dest)
 
-    env = os.environ.copy()
     # When in Tox, we need to hack things a bit so as not to have the
     # sub-interpreter `sys.executable` use the tox virtualenv's Babel
     # installation, so the locale data is where we expect it to be.
-    if "BABEL_TOX_INI_DIR" in env:
-        env["PYTHONPATH"] = env["BABEL_TOX_INI_DIR"]
+    if "BABEL_TOX_INI_DIR" in os.environ:
+        monkeypatch.setenv("PYTHONPATH", os.environ["BABEL_TOX_INI_DIR"])
 
     # Initialize an empty catalog
     subprocess.check_call([
@@ -71,7 +70,7 @@ def test_setuptools_commands(tmp_path, monkeypatch):
         "-i", os.devnull,
         "-l", "fi",
         "-d", "inited",
-    ], env=env)
+    ])
     po_file = Path("inited/fi/LC_MESSAGES/messages.po")
     orig_po_data = po_file.read_text()
     subprocess.check_call([
@@ -79,7 +78,7 @@ def test_setuptools_commands(tmp_path, monkeypatch):
         "setup.py",
         "extract_messages",
         "-o", "extracted.pot",
-    ], env=env)
+    ])
     pot_file = Path("extracted.pot")
     pot_data = pot_file.read_text()
     assert "FooBar, TM" in pot_data  # should be read from setup.cfg
@@ -90,7 +89,7 @@ def test_setuptools_commands(tmp_path, monkeypatch):
         "update_catalog",
         "-i", "extracted.pot",
         "-d", "inited",
-    ], env=env)
+    ])
     new_po_data = po_file.read_text()
     assert new_po_data != orig_po_data  # check we updated the file
     subprocess.check_call([
@@ -98,5 +97,5 @@ def test_setuptools_commands(tmp_path, monkeypatch):
         "setup.py",
         "compile_catalog",
         "-d", "inited",
-    ], env=env)
+    ])
     assert po_file.with_suffix(".mo").exists()
index f51eedb94955dc36e3b6753b8a71af737f759ac1..b6c55626cfc1509ee22e2471facf34a5635987c0 100644 (file)
@@ -42,15 +42,15 @@ def test_locale_comparison():
     assert fi_FI != bad_en_US
 
 
-def test_can_return_default_locale(os_environ):
-    os_environ['LC_MESSAGES'] = 'fr_FR.UTF-8'
+def test_can_return_default_locale(monkeypatch):
+    monkeypatch.setenv('LC_MESSAGES', 'fr_FR.UTF-8')
     assert Locale('fr', 'FR') == Locale.default('LC_MESSAGES')
 
 
-def test_ignore_invalid_locales_in_lc_ctype(os_environ):
+def test_ignore_invalid_locales_in_lc_ctype(monkeypatch):
     # This is a regression test specifically for a bad LC_CTYPE setting on
     # MacOS X 10.6 (#200)
-    os_environ['LC_CTYPE'] = 'UTF-8'
+    monkeypatch.setenv('LC_CTYPE', 'UTF-8')
     # must not throw an exception
     default_locale('LC_CTYPE')
 
@@ -76,10 +76,10 @@ class TestLocaleClass:
         assert locale.language == 'en'
         assert locale.territory == 'US'
 
-    def test_default(self, os_environ):
+    def test_default(self, monkeypatch):
         for name in ['LANGUAGE', 'LC_ALL', 'LC_CTYPE', 'LC_MESSAGES']:
-            os_environ[name] = ''
-        os_environ['LANG'] = 'fr_FR.UTF-8'
+            monkeypatch.setenv(name, '')
+        monkeypatch.setenv('LANG', 'fr_FR.UTF-8')
         default = Locale.default('LC_MESSAGES')
         assert (default.language, default.territory) == ('fr', 'FR')
 
@@ -264,17 +264,16 @@ class TestLocaleClass:
         assert Locale('ru').plural_form(100) == 'many'
 
 
-def test_default_locale(os_environ):
+def test_default_locale(monkeypatch):
     for name in ['LANGUAGE', 'LC_ALL', 'LC_CTYPE', 'LC_MESSAGES']:
-        os_environ[name] = ''
-    os_environ['LANG'] = 'fr_FR.UTF-8'
+        monkeypatch.setenv(name, '')
+    monkeypatch.setenv('LANG', 'fr_FR.UTF-8')
     assert default_locale('LC_MESSAGES') == 'fr_FR'
-
-    os_environ['LC_MESSAGES'] = 'POSIX'
+    monkeypatch.setenv('LC_MESSAGES', 'POSIX')
     assert default_locale('LC_MESSAGES') == 'en_US_POSIX'
 
     for value in ['C', 'C.UTF-8', 'POSIX']:
-        os_environ['LANGUAGE'] = value
+        monkeypatch.setenv('LANGUAGE', value)
         assert default_locale() == 'en_US_POSIX'
 
 
diff --git a/tests/test_support.py b/tests/test_support.py
deleted file mode 100644 (file)
index 44d5afb..0000000
+++ /dev/null
@@ -1,393 +0,0 @@
-#
-# Copyright (C) 2007-2011 Edgewall Software, 2013-2024 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 datetime
-import inspect
-import os
-import shutil
-import sys
-import tempfile
-import unittest
-from decimal import Decimal
-from io import BytesIO
-
-import pytest
-
-from babel import support
-from babel.messages import Catalog
-from babel.messages.mofile import write_mo
-
-SKIP_LGETTEXT = sys.version_info >= (3, 8)
-
-
-@pytest.mark.usefixtures("os_environ")
-class TranslationsTestCase(unittest.TestCase):
-
-    def setUp(self):
-        # Use a locale which won't fail to run the tests
-        os.environ['LANG'] = 'en_US.UTF-8'
-        messages1 = [
-            ('foo', {'string': 'Voh'}),
-            ('foo', {'string': 'VohCTX', 'context': 'foo'}),
-            (('foo1', 'foos1'), {'string': ('Voh1', 'Vohs1')}),
-            (('foo1', 'foos1'), {'string': ('VohCTX1', 'VohsCTX1'), 'context': 'foo'}),
-        ]
-        messages2 = [
-            ('foo', {'string': 'VohD'}),
-            ('foo', {'string': 'VohCTXD', 'context': 'foo'}),
-            (('foo1', 'foos1'), {'string': ('VohD1', 'VohsD1')}),
-            (('foo1', 'foos1'), {'string': ('VohCTXD1', 'VohsCTXD1'), 'context': 'foo'}),
-        ]
-        catalog1 = Catalog(locale='en_GB', domain='messages')
-        catalog2 = Catalog(locale='en_GB', domain='messages1')
-        for ids, kwargs in messages1:
-            catalog1.add(ids, **kwargs)
-        for ids, kwargs in messages2:
-            catalog2.add(ids, **kwargs)
-        catalog1_fp = BytesIO()
-        catalog2_fp = BytesIO()
-        write_mo(catalog1_fp, catalog1)
-        catalog1_fp.seek(0)
-        write_mo(catalog2_fp, catalog2)
-        catalog2_fp.seek(0)
-        translations1 = support.Translations(catalog1_fp)
-        translations2 = support.Translations(catalog2_fp, domain='messages1')
-        self.translations = translations1.add(translations2, merge=False)
-
-    def assertEqualTypeToo(self, expected, result):
-        assert expected == result
-        assert type(expected) is type(result), f"instance types do not match: {type(expected)!r}!={type(result)!r}"
-
-    def test_pgettext(self):
-        self.assertEqualTypeToo('Voh', self.translations.gettext('foo'))
-        self.assertEqualTypeToo('VohCTX', self.translations.pgettext('foo',
-                                                                     'foo'))
-        self.assertEqualTypeToo('VohCTX1', self.translations.pgettext('foo',
-                                                                      'foo1'))
-
-    def test_pgettext_fallback(self):
-        fallback = self.translations._fallback
-        self.translations._fallback = support.NullTranslations()
-        assert self.translations.pgettext('foo', 'bar') == 'bar'
-        self.translations._fallback = fallback
-
-    def test_upgettext(self):
-        self.assertEqualTypeToo('Voh', self.translations.ugettext('foo'))
-        self.assertEqualTypeToo('VohCTX', self.translations.upgettext('foo',
-                                                                      'foo'))
-
-    @pytest.mark.skipif(SKIP_LGETTEXT, reason="lgettext is deprecated")
-    def test_lpgettext(self):
-        self.assertEqualTypeToo(b'Voh', self.translations.lgettext('foo'))
-        self.assertEqualTypeToo(b'VohCTX', self.translations.lpgettext('foo',
-                                                                       'foo'))
-
-    def test_npgettext(self):
-        self.assertEqualTypeToo('Voh1',
-                                self.translations.ngettext('foo1', 'foos1', 1))
-        self.assertEqualTypeToo('Vohs1',
-                                self.translations.ngettext('foo1', 'foos1', 2))
-        self.assertEqualTypeToo('VohCTX1',
-                                self.translations.npgettext('foo', 'foo1',
-                                                            'foos1', 1))
-        self.assertEqualTypeToo('VohsCTX1',
-                                self.translations.npgettext('foo', 'foo1',
-                                                            'foos1', 2))
-
-    def test_unpgettext(self):
-        self.assertEqualTypeToo('Voh1',
-                                self.translations.ungettext('foo1', 'foos1', 1))
-        self.assertEqualTypeToo('Vohs1',
-                                self.translations.ungettext('foo1', 'foos1', 2))
-        self.assertEqualTypeToo('VohCTX1',
-                                self.translations.unpgettext('foo', 'foo1',
-                                                             'foos1', 1))
-        self.assertEqualTypeToo('VohsCTX1',
-                                self.translations.unpgettext('foo', 'foo1',
-                                                             'foos1', 2))
-
-    @pytest.mark.skipif(SKIP_LGETTEXT, reason="lgettext is deprecated")
-    def test_lnpgettext(self):
-        self.assertEqualTypeToo(b'Voh1',
-                                self.translations.lngettext('foo1', 'foos1', 1))
-        self.assertEqualTypeToo(b'Vohs1',
-                                self.translations.lngettext('foo1', 'foos1', 2))
-        self.assertEqualTypeToo(b'VohCTX1',
-                                self.translations.lnpgettext('foo', 'foo1',
-                                                             'foos1', 1))
-        self.assertEqualTypeToo(b'VohsCTX1',
-                                self.translations.lnpgettext('foo', 'foo1',
-                                                             'foos1', 2))
-
-    def test_dpgettext(self):
-        self.assertEqualTypeToo(
-            'VohD', self.translations.dgettext('messages1', 'foo'))
-        self.assertEqualTypeToo(
-            'VohCTXD', self.translations.dpgettext('messages1', 'foo', 'foo'))
-
-    def test_dupgettext(self):
-        self.assertEqualTypeToo(
-            'VohD', self.translations.dugettext('messages1', 'foo'))
-        self.assertEqualTypeToo(
-            'VohCTXD', self.translations.dupgettext('messages1', 'foo', 'foo'))
-
-    @pytest.mark.skipif(SKIP_LGETTEXT, reason="lgettext is deprecated")
-    def test_ldpgettext(self):
-        self.assertEqualTypeToo(
-            b'VohD', self.translations.ldgettext('messages1', 'foo'))
-        self.assertEqualTypeToo(
-            b'VohCTXD', self.translations.ldpgettext('messages1', 'foo', 'foo'))
-
-    def test_dnpgettext(self):
-        self.assertEqualTypeToo(
-            'VohD1', self.translations.dngettext('messages1', 'foo1', 'foos1', 1))
-        self.assertEqualTypeToo(
-            'VohsD1', self.translations.dngettext('messages1', 'foo1', 'foos1', 2))
-        self.assertEqualTypeToo(
-            'VohCTXD1', self.translations.dnpgettext('messages1', 'foo', 'foo1',
-                                                     'foos1', 1))
-        self.assertEqualTypeToo(
-            'VohsCTXD1', self.translations.dnpgettext('messages1', 'foo', 'foo1',
-                                                      'foos1', 2))
-
-    def test_dunpgettext(self):
-        self.assertEqualTypeToo(
-            'VohD1', self.translations.dungettext('messages1', 'foo1', 'foos1', 1))
-        self.assertEqualTypeToo(
-            'VohsD1', self.translations.dungettext('messages1', 'foo1', 'foos1', 2))
-        self.assertEqualTypeToo(
-            'VohCTXD1', self.translations.dunpgettext('messages1', 'foo', 'foo1',
-                                                      'foos1', 1))
-        self.assertEqualTypeToo(
-            'VohsCTXD1', self.translations.dunpgettext('messages1', 'foo', 'foo1',
-                                                       'foos1', 2))
-
-    @pytest.mark.skipif(SKIP_LGETTEXT, reason="lgettext is deprecated")
-    def test_ldnpgettext(self):
-        self.assertEqualTypeToo(
-            b'VohD1', self.translations.ldngettext('messages1', 'foo1', 'foos1', 1))
-        self.assertEqualTypeToo(
-            b'VohsD1', self.translations.ldngettext('messages1', 'foo1', 'foos1', 2))
-        self.assertEqualTypeToo(
-            b'VohCTXD1', self.translations.ldnpgettext('messages1', 'foo', 'foo1',
-                                                       'foos1', 1))
-        self.assertEqualTypeToo(
-            b'VohsCTXD1', self.translations.ldnpgettext('messages1', 'foo', 'foo1',
-                                                        'foos1', 2))
-
-    def test_load(self):
-        tempdir = tempfile.mkdtemp()
-        try:
-            messages_dir = os.path.join(tempdir, 'fr', 'LC_MESSAGES')
-            os.makedirs(messages_dir)
-            catalog = Catalog(locale='fr', domain='messages')
-            catalog.add('foo', 'bar')
-            with open(os.path.join(messages_dir, 'messages.mo'), 'wb') as f:
-                write_mo(f, catalog)
-
-            translations = support.Translations.load(tempdir, locales=('fr',), domain='messages')
-            assert translations.gettext('foo') == 'bar'
-        finally:
-            shutil.rmtree(tempdir)
-
-
-class NullTranslationsTestCase(unittest.TestCase):
-
-    def setUp(self):
-        fp = BytesIO()
-        write_mo(fp, Catalog(locale='de'))
-        fp.seek(0)
-        self.translations = support.Translations(fp=fp)
-        self.null_translations = support.NullTranslations(fp=fp)
-
-    def method_names(self):
-        names = [name for name in dir(self.translations) if 'gettext' in name]
-        if SKIP_LGETTEXT:
-            # Remove deprecated l*gettext functions
-            names = [name for name in names if not name.startswith('l')]
-        return names
-
-    def test_same_methods(self):
-        for name in self.method_names():
-            if not hasattr(self.null_translations, name):
-                self.fail(f"NullTranslations does not provide method {name!r}")
-
-    def test_method_signature_compatibility(self):
-        for name in self.method_names():
-            translations_method = getattr(self.translations, name)
-            null_method = getattr(self.null_translations, name)
-            assert inspect.getfullargspec(translations_method) == inspect.getfullargspec(null_method)
-
-    def test_same_return_values(self):
-        data = {
-            'message': 'foo', 'domain': 'domain', 'context': 'tests',
-            'singular': 'bar', 'plural': 'baz', 'num': 1,
-            'msgid1': 'bar', 'msgid2': 'baz', 'n': 1,
-        }
-        for name in self.method_names():
-            method = getattr(self.translations, name)
-            null_method = getattr(self.null_translations, name)
-            signature = inspect.getfullargspec(method)
-            parameter_names = [name for name in signature.args if name != 'self']
-            values = [data[name] for name in parameter_names]
-            assert method(*values) == null_method(*values)
-
-
-class LazyProxyTestCase(unittest.TestCase):
-
-    def test_proxy_caches_result_of_function_call(self):
-        self.counter = 0
-
-        def add_one():
-            self.counter += 1
-            return self.counter
-        proxy = support.LazyProxy(add_one)
-        assert proxy.value == 1
-        assert proxy.value == 1
-
-    def test_can_disable_proxy_cache(self):
-        self.counter = 0
-
-        def add_one():
-            self.counter += 1
-            return self.counter
-        proxy = support.LazyProxy(add_one, enable_cache=False)
-        assert proxy.value == 1
-        assert proxy.value == 2
-
-    def test_can_copy_proxy(self):
-        from copy import copy
-
-        numbers = [1, 2]
-
-        def first(xs):
-            return xs[0]
-
-        proxy = support.LazyProxy(first, numbers)
-        proxy_copy = copy(proxy)
-
-        numbers.pop(0)
-        assert proxy.value == 2
-        assert proxy_copy.value == 2
-
-    def test_can_deepcopy_proxy(self):
-        from copy import deepcopy
-        numbers = [1, 2]
-
-        def first(xs):
-            return xs[0]
-
-        proxy = support.LazyProxy(first, numbers)
-        proxy_deepcopy = deepcopy(proxy)
-
-        numbers.pop(0)
-        assert proxy.value == 2
-        assert proxy_deepcopy.value == 1
-
-    def test_handle_attribute_error(self):
-
-        def raise_attribute_error():
-            raise AttributeError('message')
-
-        proxy = support.LazyProxy(raise_attribute_error)
-        with pytest.raises(AttributeError, match='message'):
-            _ = proxy.value
-
-
-class TestFormat:
-    def test_format_datetime(self, timezone_getter):
-        when = datetime.datetime(2007, 4, 1, 15, 30)
-        fmt = support.Format('en_US', tzinfo=timezone_getter('US/Eastern'))
-        assert fmt.datetime(when) == 'Apr 1, 2007, 11:30:00\u202fAM'
-
-    def test_format_time(self, timezone_getter):
-        when = datetime.datetime(2007, 4, 1, 15, 30)
-        fmt = support.Format('en_US', tzinfo=timezone_getter('US/Eastern'))
-        assert fmt.time(when) == '11:30:00\u202fAM'
-
-    def test_format_number(self):
-        assert support.Format('en_US').number(1234) == '1,234'
-        assert support.Format('ar_EG', numbering_system="default").number(1234) == '1٬234'
-
-    def test_format_decimal(self):
-        assert support.Format('en_US').decimal(1234.5) == '1,234.5'
-        assert support.Format('en_US').decimal(Decimal("1234.5")) == '1,234.5'
-        assert support.Format('ar_EG', numbering_system="default").decimal(1234.5) == '1٬234٫5'
-        assert support.Format('ar_EG', numbering_system="default").decimal(Decimal("1234.5")) == '1٬234٫5'
-
-    def test_format_compact_decimal(self):
-        assert support.Format('en_US').compact_decimal(1234) == '1K'
-        assert support.Format('ar_EG', numbering_system="default").compact_decimal(
-            1234, fraction_digits=1) == '1٫2\xa0ألف'
-        assert support.Format('ar_EG', numbering_system="default").compact_decimal(
-            Decimal("1234"), fraction_digits=1) == '1٫2\xa0ألف'
-
-    def test_format_currency(self):
-        assert support.Format('en_US').currency(1099.98, 'USD') == '$1,099.98'
-        assert support.Format('en_US').currency(Decimal("1099.98"), 'USD') == '$1,099.98'
-        assert support.Format('ar_EG', numbering_system="default").currency(
-            1099.98, 'EGP') == '\u200f1٬099٫98\xa0ج.م.\u200f'
-
-    def test_format_compact_currency(self):
-        assert support.Format('en_US').compact_currency(1099.98, 'USD') == '$1K'
-        assert support.Format('en_US').compact_currency(Decimal("1099.98"), 'USD') == '$1K'
-        assert support.Format('ar_EG', numbering_system="default").compact_currency(
-            1099.98, 'EGP') == '1\xa0ألف\xa0ج.م.\u200f'
-
-    def test_format_percent(self):
-        assert support.Format('en_US').percent(0.34) == '34%'
-        assert support.Format('en_US').percent(Decimal("0.34")) == '34%'
-        assert support.Format('ar_EG', numbering_system="default").percent(134.5) == '13٬450%'
-
-    def test_format_scientific(self):
-        assert support.Format('en_US').scientific(10000) == '1E4'
-        assert support.Format('en_US').scientific(Decimal("10000")) == '1E4'
-        assert support.Format('ar_EG', numbering_system="default").scientific(10000) == '1أس4'
-
-
-def test_lazy_proxy():
-    def greeting(name='world'):
-        return f"Hello, {name}!"
-
-    lazy_greeting = support.LazyProxy(greeting, name='Joe')
-    assert str(lazy_greeting) == "Hello, Joe!"
-    assert '  ' + lazy_greeting == '  Hello, Joe!'
-    assert '(%s)' % lazy_greeting == '(Hello, Joe!)'
-    assert f"[{lazy_greeting}]" == "[Hello, Joe!]"
-
-    greetings = sorted([
-        support.LazyProxy(greeting, 'world'),
-        support.LazyProxy(greeting, 'Joe'),
-        support.LazyProxy(greeting, 'universe'),
-    ])
-    assert [str(g) for g in greetings] == [
-        "Hello, Joe!",
-        "Hello, universe!",
-        "Hello, world!",
-    ]
-
-
-def test_catalog_merge_files():
-    # Refs issues #92, #162
-    t1 = support.Translations()
-    assert t1.files == []
-    t1._catalog["foo"] = "bar"
-    fp = BytesIO()
-    write_mo(fp, Catalog())
-    fp.seek(0)
-    fp.name = "pro.mo"
-    t2 = support.Translations(fp)
-    assert t2.files == ["pro.mo"]
-    t2._catalog["bar"] = "quux"
-    t1.merge(t2)
-    assert t1.files == ["pro.mo"]
-    assert set(t1._catalog.keys()) == {'', 'foo', 'bar'}
diff --git a/tests/test_support_format.py b/tests/test_support_format.py
new file mode 100644 (file)
index 0000000..fdfac84
--- /dev/null
@@ -0,0 +1,68 @@
+import datetime
+from decimal import Decimal
+
+import pytest
+
+from babel import support
+
+
+@pytest.fixture
+def ar_eg_format() -> support.Format:
+    return support.Format('ar_EG', numbering_system="default")
+
+
+@pytest.fixture
+def en_us_format(timezone_getter) -> support.Format:
+    return support.Format('en_US', tzinfo=timezone_getter('US/Eastern'))
+
+
+def test_format_datetime(en_us_format):
+    when = datetime.datetime(2007, 4, 1, 15, 30)
+    assert en_us_format.datetime(when) == 'Apr 1, 2007, 11:30:00\u202fAM'
+
+
+def test_format_time(en_us_format):
+    when = datetime.datetime(2007, 4, 1, 15, 30)
+    assert en_us_format.time(when) == '11:30:00\u202fAM'
+
+
+def test_format_number(ar_eg_format, en_us_format):
+    assert en_us_format.number(1234) == '1,234'
+    assert ar_eg_format.number(1234) == '1٬234'
+
+
+def test_format_decimal(ar_eg_format, en_us_format):
+    assert en_us_format.decimal(1234.5) == '1,234.5'
+    assert en_us_format.decimal(Decimal("1234.5")) == '1,234.5'
+    assert ar_eg_format.decimal(1234.5) == '1٬234٫5'
+    assert ar_eg_format.decimal(Decimal("1234.5")) == '1٬234٫5'
+
+
+def test_format_compact_decimal(ar_eg_format, en_us_format):
+    assert en_us_format.compact_decimal(1234) == '1K'
+    assert ar_eg_format.compact_decimal(1234, fraction_digits=1) == '1٫2\xa0ألف'
+    assert ar_eg_format.compact_decimal(Decimal("1234"), fraction_digits=1) == '1٫2\xa0ألف'
+
+
+def test_format_currency(ar_eg_format, en_us_format):
+    assert en_us_format.currency(1099.98, 'USD') == '$1,099.98'
+    assert en_us_format.currency(Decimal("1099.98"), 'USD') == '$1,099.98'
+    assert ar_eg_format.currency(1099.98, 'EGP') == '\u200f1٬099٫98\xa0ج.م.\u200f'
+
+
+def test_format_compact_currency(ar_eg_format, en_us_format):
+    assert en_us_format.compact_currency(1099.98, 'USD') == '$1K'
+    assert en_us_format.compact_currency(Decimal("1099.98"), 'USD') == '$1K'
+    assert ar_eg_format.compact_currency(1099.98, 'EGP') == '1\xa0ألف\xa0ج.م.\u200f'
+
+
+def test_format_percent(ar_eg_format, en_us_format):
+    assert en_us_format.percent(0.34) == '34%'
+    assert en_us_format.percent(Decimal("0.34")) == '34%'
+    assert ar_eg_format.percent(134.5) == '13٬450%'
+
+
+def test_format_scientific(ar_eg_format, en_us_format):
+    assert en_us_format.scientific(10000) == '1E4'
+    assert en_us_format.scientific(Decimal("10000")) == '1E4'
+    assert ar_eg_format.scientific(10000) == '1أس4'
diff --git a/tests/test_support_lazy_proxy.py b/tests/test_support_lazy_proxy.py
new file mode 100644 (file)
index 0000000..8445f71
--- /dev/null
@@ -0,0 +1,80 @@
+import copy
+
+import pytest
+
+from babel import support
+
+
+def test_proxy_caches_result_of_function_call():
+    counter = 0
+
+    def add_one():
+        nonlocal counter
+        counter += 1
+        return counter
+
+    proxy = support.LazyProxy(add_one)
+    assert proxy.value == 1
+    assert proxy.value == 1
+
+
+def test_can_disable_proxy_cache():
+    counter = 0
+
+    def add_one():
+        nonlocal counter
+        counter += 1
+        return counter
+
+    proxy = support.LazyProxy(add_one, enable_cache=False)
+    assert proxy.value == 1
+    assert proxy.value == 2
+
+
+@pytest.mark.parametrize(("copier", "expected_copy_value"), [
+    (copy.copy, 2),
+    (copy.deepcopy, 1),
+])
+def test_can_copy_proxy(copier, expected_copy_value):
+    numbers = [1, 2]
+
+    def first(xs):
+        return xs[0]
+
+    proxy = support.LazyProxy(first, numbers)
+    proxy_copy = copier(proxy)
+
+    numbers.pop(0)
+    assert proxy.value == 2
+    assert proxy_copy.value == expected_copy_value
+
+
+def test_handle_attribute_error():
+    def raise_attribute_error():
+        raise AttributeError('message')
+
+    proxy = support.LazyProxy(raise_attribute_error)
+    with pytest.raises(AttributeError, match='message'):
+        _ = proxy.value
+
+
+def test_lazy_proxy():
+    def greeting(name='world'):
+        return f"Hello, {name}!"
+
+    lazy_greeting = support.LazyProxy(greeting, name='Joe')
+    assert str(lazy_greeting) == "Hello, Joe!"
+    assert '  ' + lazy_greeting == '  Hello, Joe!'
+    assert '(%s)' % lazy_greeting == '(Hello, Joe!)'
+    assert f"[{lazy_greeting}]" == "[Hello, Joe!]"
+
+    greetings = sorted([
+        support.LazyProxy(greeting, 'world'),
+        support.LazyProxy(greeting, 'Joe'),
+        support.LazyProxy(greeting, 'universe'),
+    ])
+    assert [str(g) for g in greetings] == [
+        "Hello, Joe!",
+        "Hello, universe!",
+        "Hello, world!",
+    ]
diff --git a/tests/test_support_translations.py b/tests/test_support_translations.py
new file mode 100644 (file)
index 0000000..7e6dc59
--- /dev/null
@@ -0,0 +1,235 @@
+import inspect
+import io
+import os
+import shutil
+import sys
+import tempfile
+
+import pytest
+
+from babel import support
+from babel.messages import Catalog
+from babel.messages.mofile import write_mo
+
+SKIP_LGETTEXT = sys.version_info >= (3, 8)
+
+messages1 = [
+    ('foo', {'string': 'Voh'}),
+    ('foo', {'string': 'VohCTX', 'context': 'foo'}),
+    (('foo1', 'foos1'), {'string': ('Voh1', 'Vohs1')}),
+    (('foo1', 'foos1'), {'string': ('VohCTX1', 'VohsCTX1'), 'context': 'foo'}),
+]
+
+messages2 = [
+    ('foo', {'string': 'VohD'}),
+    ('foo', {'string': 'VohCTXD', 'context': 'foo'}),
+    (('foo1', 'foos1'), {'string': ('VohD1', 'VohsD1')}),
+    (('foo1', 'foos1'), {'string': ('VohCTXD1', 'VohsCTXD1'), 'context': 'foo'}),
+]
+
+
+@pytest.fixture(autouse=True)
+def use_en_us_locale(monkeypatch):
+    # Use a locale which won't fail to run the tests
+    monkeypatch.setenv('LANG', 'en_US.UTF-8')
+
+
+@pytest.fixture()
+def translations() -> support.Translations:
+    catalog1 = Catalog(locale='en_GB', domain='messages')
+    catalog2 = Catalog(locale='en_GB', domain='messages1')
+    for ids, kwargs in messages1:
+        catalog1.add(ids, **kwargs)
+    for ids, kwargs in messages2:
+        catalog2.add(ids, **kwargs)
+    catalog1_fp = io.BytesIO()
+    catalog2_fp = io.BytesIO()
+    write_mo(catalog1_fp, catalog1)
+    catalog1_fp.seek(0)
+    write_mo(catalog2_fp, catalog2)
+    catalog2_fp.seek(0)
+    translations1 = support.Translations(catalog1_fp)
+    translations2 = support.Translations(catalog2_fp, domain='messages1')
+    return translations1.add(translations2, merge=False)
+
+
+@pytest.fixture(scope='module')
+def empty_translations() -> support.Translations:
+    fp = io.BytesIO()
+    write_mo(fp, Catalog(locale='de'))
+    fp.seek(0)
+    return support.Translations(fp=fp)
+
+
+@pytest.fixture(scope='module')
+def null_translations() -> support.NullTranslations:
+    fp = io.BytesIO()
+    write_mo(fp, Catalog(locale='de'))
+    fp.seek(0)
+    return support.NullTranslations(fp=fp)
+
+
+def assert_equal_type_too(expected, result) -> None:
+    assert expected == result
+    assert type(expected) is type(result), (
+        f'instance types do not match: {type(expected)!r}!={type(result)!r}'
+    )
+
+
+def test_pgettext(translations):
+    assert_equal_type_too('Voh', translations.gettext('foo'))
+    assert_equal_type_too('VohCTX', translations.pgettext('foo', 'foo'))
+    assert_equal_type_too('VohCTX1', translations.pgettext('foo', 'foo1'))
+
+
+def test_pgettext_fallback(translations):
+    fallback = translations._fallback
+    translations._fallback = support.NullTranslations()
+    assert translations.pgettext('foo', 'bar') == 'bar'
+    translations._fallback = fallback
+
+
+def test_upgettext(translations):
+    assert_equal_type_too('Voh', translations.ugettext('foo'))
+    assert_equal_type_too('VohCTX', translations.upgettext('foo', 'foo'))
+
+
+@pytest.mark.skipif(SKIP_LGETTEXT, reason='lgettext is deprecated')
+def test_lpgettext(translations):
+    assert_equal_type_too(b'Voh', translations.lgettext('foo'))
+    assert_equal_type_too(b'VohCTX', translations.lpgettext('foo', 'foo'))
+
+
+def test_npgettext(translations):
+    assert_equal_type_too('Voh1', translations.ngettext('foo1', 'foos1', 1))
+    assert_equal_type_too('Vohs1', translations.ngettext('foo1', 'foos1', 2))
+    assert_equal_type_too('VohCTX1', translations.npgettext('foo', 'foo1', 'foos1', 1))
+    assert_equal_type_too('VohsCTX1', translations.npgettext('foo', 'foo1', 'foos1', 2))
+
+
+def test_unpgettext(translations):
+    assert_equal_type_too('Voh1', translations.ungettext('foo1', 'foos1', 1))
+    assert_equal_type_too('Vohs1', translations.ungettext('foo1', 'foos1', 2))
+    assert_equal_type_too('VohCTX1', translations.unpgettext('foo', 'foo1', 'foos1', 1))
+    assert_equal_type_too('VohsCTX1', translations.unpgettext('foo', 'foo1', 'foos1', 2))
+
+
+@pytest.mark.skipif(SKIP_LGETTEXT, reason='lgettext is deprecated')
+def test_lnpgettext(translations):
+    assert_equal_type_too(b'Voh1', translations.lngettext('foo1', 'foos1', 1))
+    assert_equal_type_too(b'Vohs1', translations.lngettext('foo1', 'foos1', 2))
+    assert_equal_type_too(b'VohCTX1', translations.lnpgettext('foo', 'foo1', 'foos1', 1))
+    assert_equal_type_too(b'VohsCTX1', translations.lnpgettext('foo', 'foo1', 'foos1', 2))
+
+
+def test_dpgettext(translations):
+    assert_equal_type_too('VohD', translations.dgettext('messages1', 'foo'))
+    assert_equal_type_too('VohCTXD', translations.dpgettext('messages1', 'foo', 'foo'))
+
+
+def test_dupgettext(translations):
+    assert_equal_type_too('VohD', translations.dugettext('messages1', 'foo'))
+    assert_equal_type_too('VohCTXD', translations.dupgettext('messages1', 'foo', 'foo'))
+
+
+@pytest.mark.skipif(SKIP_LGETTEXT, reason='lgettext is deprecated')
+def test_ldpgettext(translations):
+    assert_equal_type_too(b'VohD', translations.ldgettext('messages1', 'foo'))
+    assert_equal_type_too(b'VohCTXD', translations.ldpgettext('messages1', 'foo', 'foo'))
+
+
+def test_dnpgettext(translations):
+    assert_equal_type_too('VohD1', translations.dngettext('messages1', 'foo1', 'foos1', 1))
+    assert_equal_type_too('VohsD1', translations.dngettext('messages1', 'foo1', 'foos1', 2))
+    assert_equal_type_too('VohCTXD1', translations.dnpgettext('messages1', 'foo', 'foo1', 'foos1', 1))
+    assert_equal_type_too('VohsCTXD1', translations.dnpgettext('messages1', 'foo', 'foo1', 'foos1', 2))
+
+
+def test_dunpgettext(translations):
+    assert_equal_type_too('VohD1', translations.dungettext('messages1', 'foo1', 'foos1', 1))
+    assert_equal_type_too('VohsD1', translations.dungettext('messages1', 'foo1', 'foos1', 2))
+    assert_equal_type_too('VohCTXD1', translations.dunpgettext('messages1', 'foo', 'foo1', 'foos1', 1))
+    assert_equal_type_too('VohsCTXD1', translations.dunpgettext('messages1', 'foo', 'foo1', 'foos1', 2))
+
+
+@pytest.mark.skipif(SKIP_LGETTEXT, reason='lgettext is deprecated')
+def test_ldnpgettext(translations):
+    assert_equal_type_too(b'VohD1', translations.ldngettext('messages1', 'foo1', 'foos1', 1))
+    assert_equal_type_too(b'VohsD1', translations.ldngettext('messages1', 'foo1', 'foos1', 2))
+    assert_equal_type_too(b'VohCTXD1', translations.ldnpgettext('messages1', 'foo', 'foo1', 'foos1', 1))
+    assert_equal_type_too(b'VohsCTXD1', translations.ldnpgettext('messages1', 'foo', 'foo1', 'foos1', 2))
+
+
+def test_load(translations):
+    tempdir = tempfile.mkdtemp()
+    try:
+        messages_dir = os.path.join(tempdir, 'fr', 'LC_MESSAGES')
+        os.makedirs(messages_dir)
+        catalog = Catalog(locale='fr', domain='messages')
+        catalog.add('foo', 'bar')
+        with open(os.path.join(messages_dir, 'messages.mo'), 'wb') as f:
+            write_mo(f, catalog)
+
+        translations = support.Translations.load(tempdir, locales=('fr',), domain='messages')
+        assert translations.gettext('foo') == 'bar'
+    finally:
+        shutil.rmtree(tempdir)
+
+
+def get_gettext_method_names(obj):
+    names = [name for name in dir(obj) if 'gettext' in name]
+    if SKIP_LGETTEXT:
+        # Remove deprecated l*gettext functions
+        names = [name for name in names if not name.startswith('l')]
+    return names
+
+
+def test_null_translations_have_same_methods(empty_translations, null_translations):
+    for name in get_gettext_method_names(empty_translations):
+        assert hasattr(null_translations, name), f'NullTranslations does not provide method {name!r}'
+
+
+def test_null_translations_method_signature_compatibility(empty_translations, null_translations):
+    for name in get_gettext_method_names(empty_translations):
+        assert (
+            inspect.getfullargspec(getattr(empty_translations, name)) ==
+            inspect.getfullargspec(getattr(null_translations, name))
+        )
+
+
+def test_null_translations_same_return_values(empty_translations, null_translations):
+    data = {
+        'message': 'foo',
+        'domain': 'domain',
+        'context': 'tests',
+        'singular': 'bar',
+        'plural': 'baz',
+        'num': 1,
+        'msgid1': 'bar',
+        'msgid2': 'baz',
+        'n': 1,
+    }
+    for name in get_gettext_method_names(empty_translations):
+        method = getattr(empty_translations, name)
+        null_method = getattr(null_translations, name)
+        signature = inspect.getfullargspec(method)
+        parameter_names = [name for name in signature.args if name != 'self']
+        values = [data[name] for name in parameter_names]
+        assert method(*values) == null_method(*values)
+
+
+def test_catalog_merge_files():
+    # Refs issues #92, #162
+    t1 = support.Translations()
+    assert t1.files == []
+    t1._catalog['foo'] = 'bar'
+    fp = io.BytesIO()
+    write_mo(fp, Catalog())
+    fp.seek(0)
+    fp.name = 'pro.mo'
+    t2 = support.Translations(fp)
+    assert t2.files == ['pro.mo']
+    t2._catalog['bar'] = 'quux'
+    t1.merge(t2)
+    assert t1.files == ['pro.mo']
+    assert set(t1._catalog.keys()) == {'', 'foo', 'bar'}