changeset 596:f63a07d648b6 trunk

add babel.support.NullTranslations class similar to gettext.NullTranslations but with all of Babel's new *gettext methods (#277)
author fschwarz
date Mon, 20 Aug 2012 19:21:22 +0000
parents 57a08cc52623
children 92e3eb0a317a
files ChangeLog babel/support.py babel/tests/support.py
diffstat 3 files changed, 152 insertions(+), 91 deletions(-) [+]
line wrap: on
line diff
--- a/ChangeLog
+++ b/ChangeLog
@@ -52,6 +52,8 @@
  * resort to hard-coded message extractors/checkers if pkg_resources is 
    installed but no egg-info was found (#230)
  * format_time() and format_datetime() now accept also floats (#242)
+ * add babel.support.NullTranslations class similar to gettext.NullTranslations
+   but with all of Babel's new *gettext methods (#277)
 
 
 Version 0.9.6
--- a/babel/support.py
+++ b/babel/support.py
@@ -28,7 +28,7 @@
                           format_percent, format_scientific
 from babel.util import UTC
 
-__all__ = ['Format', 'LazyProxy', 'Translations']
+__all__ = ['Format', 'LazyProxy', 'NullTranslations', 'Translations']
 __docformat__ = 'restructuredtext en'
 
 
@@ -281,101 +281,27 @@
     def __setitem__(self, key, value):
         self.value[key] = value
 
-    
-class Translations(gettext.GNUTranslations, object):
-    """An extended translation catalog class."""
-
-    DEFAULT_DOMAIN = 'messages'
-
-    def __init__(self, fileobj=None, domain=None):
-        """Initialize the translations catalog.
-
-        :param fileobj: the file-like object the translation should be read
-                        from
-        :param domain: the message domain (default: 'messages')
-        """
-        if domain is None:
-            domain = self.DEFAULT_DOMAIN
-        gettext.GNUTranslations.__init__(self, fp=fileobj)
-        self.files = filter(None, [getattr(fileobj, 'name', None)])
-        self.domain = domain
-        self._domains = {}
-
-    @classmethod
-    def load(cls, dirname=None, locales=None, domain=None):
-        """Load translations from the given directory.
-
-        :param dirname: the directory containing the ``MO`` files
-        :param locales: the list of locales in order of preference (items in
-                        this list can be either `Locale` objects or locale
-                        strings)
-        :param domain: the message domain (default: 'messages')
-        :return: the loaded catalog, or a ``NullTranslations`` instance if no
-                 matching translations were found
-        :rtype: `Translations`
-        """
-        if locales is not None:
-            if not isinstance(locales, (list, tuple)):
-                locales = [locales]
-            locales = [str(locale) for locale in locales]
-        if not domain:
-            domain = cls.DEFAULT_DOMAIN
-        filename = gettext.find(domain, dirname, locales)
-        if not filename:
-            return gettext.NullTranslations()
-        return cls(fileobj=open(filename, 'rb'), domain=domain)
 
-    def __repr__(self):
-        return '<%s: "%s">' % (type(self).__name__,
-                               self._info.get('project-id-version'))
-
-    def add(self, translations, merge=True):
-        """Add the given translations to the catalog.
-
-        If the domain of the translations is different than that of the
-        current catalog, they are added as a catalog that is only accessible
-        by the various ``d*gettext`` functions.
-
-        :param translations: the `Translations` instance with the messages to
-                             add
-        :param merge: whether translations for message domains that have
-                      already been added should be merged with the existing
-                      translations
-        :return: the `Translations` instance (``self``) so that `merge` calls
-                 can be easily chained
-        :rtype: `Translations`
-        """
-        domain = getattr(translations, 'domain', self.DEFAULT_DOMAIN)
-        if merge and domain == self.domain:
-            return self.merge(translations)
+class NullTranslations(gettext.NullTranslations, object): 
 
-        existing = self._domains.get(domain)
-        if merge and existing is not None:
-            existing.merge(translations)
-        else:
-            translations.add_fallback(self)
-            self._domains[domain] = translations
-
-        return self
-
-    def merge(self, translations):
-        """Merge the given translations into the catalog.
+    DEFAULT_DOMAIN = None
 
-        Message translations in the specified catalog override any messages
-        with the same identifier in the existing catalog.
+    def __init__(self, fp=None):
+        """Initialize a simple translations class which is not backed by a
+        real catalog. Behaves similar to gettext.NullTranslations but also
+        offers Babel's on *gettext methods (e.g. 'dgettext()').
 
-        :param translations: the `Translations` instance with the messages to
-                             merge
-        :return: the `Translations` instance (``self``) so that `merge` calls
-                 can be easily chained
-        :rtype: `Translations`
+        :param fp: a file-like object (ignored in this class)
         """
-        if isinstance(translations, gettext.GNUTranslations):
-            self._catalog.update(translations._catalog)
-            if isinstance(translations, Translations):
-                self.files.extend(translations.files)
-
-        return self
+        # These attributes are set by gettext.NullTranslations when a catalog
+        # is parsed (fp != None). Ensure that they are always present because
+        # some *gettext methods (including '.gettext()') rely on the attributes.
+        self._catalog = {}
+        self.plural = lambda n: int(n != 1)
+        super(NullTranslations, self).__init__(fp=fp)
+        self.files = filter(None, [getattr(fp, 'name', None)])
+        self.domain = self.DEFAULT_DOMAIN
+        self._domains = {}
 
     def dgettext(self, domain, message):
         """Like ``gettext()``, but look the message up in the specified
@@ -592,3 +518,95 @@
         return self._domains.get(domain, self).lnpgettext(context, singular,
                                                           plural, num)
 
+
+class Translations(NullTranslations, gettext.GNUTranslations):
+    """An extended translation catalog class."""
+
+    DEFAULT_DOMAIN = 'messages'
+
+    def __init__(self, fileobj=None, domain=None):
+        """Initialize the translations catalog.
+
+        :param fileobj: the file-like object the translation should be read
+                        from
+        :param domain: the message domain (default: 'messages')
+        """
+        super(Translations, self).__init__(fp=fileobj)
+        self.domain = domain or self.DEFAULT_DOMAIN
+
+    @classmethod
+    def load(cls, dirname=None, locales=None, domain=None):
+        """Load translations from the given directory.
+
+        :param dirname: the directory containing the ``MO`` files
+        :param locales: the list of locales in order of preference (items in
+                        this list can be either `Locale` objects or locale
+                        strings)
+        :param domain: the message domain (default: 'messages')
+        :return: the loaded catalog, or a ``NullTranslations`` instance if no
+                 matching translations were found
+        :rtype: `Translations`
+        """
+        if locales is not None:
+            if not isinstance(locales, (list, tuple)):
+                locales = [locales]
+            locales = [str(locale) for locale in locales]
+        if not domain:
+            domain = cls.DEFAULT_DOMAIN
+        filename = gettext.find(domain, dirname, locales)
+        if not filename:
+            return gettext.NullTranslations()
+        return cls(fileobj=open(filename, 'rb'), domain=domain)
+
+    def __repr__(self):
+        return '<%s: "%s">' % (type(self).__name__,
+                               self._info.get('project-id-version'))
+
+    def add(self, translations, merge=True):
+        """Add the given translations to the catalog.
+
+        If the domain of the translations is different than that of the
+        current catalog, they are added as a catalog that is only accessible
+        by the various ``d*gettext`` functions.
+
+        :param translations: the `Translations` instance with the messages to
+                             add
+        :param merge: whether translations for message domains that have
+                      already been added should be merged with the existing
+                      translations
+        :return: the `Translations` instance (``self``) so that `merge` calls
+                 can be easily chained
+        :rtype: `Translations`
+        """
+        domain = getattr(translations, 'domain', self.DEFAULT_DOMAIN)
+        if merge and domain == self.domain:
+            return self.merge(translations)
+
+        existing = self._domains.get(domain)
+        if merge and existing is not None:
+            existing.merge(translations)
+        else:
+            translations.add_fallback(self)
+            self._domains[domain] = translations
+
+        return self
+
+    def merge(self, translations):
+        """Merge the given translations into the catalog.
+
+        Message translations in the specified catalog override any messages
+        with the same identifier in the existing catalog.
+
+        :param translations: the `Translations` instance with the messages to
+                             merge
+        :return: the `Translations` instance (``self``) so that `merge` calls
+                 can be easily chained
+        :rtype: `Translations`
+        """
+        if isinstance(translations, gettext.GNUTranslations):
+            self._catalog.update(translations._catalog)
+            if isinstance(translations, Translations):
+                self.files.extend(translations.files)
+
+        return self
+
--- a/babel/tests/support.py
+++ b/babel/tests/support.py
@@ -12,6 +12,7 @@
 # history and logs, available at http://babel.edgewall.org/log/.
 
 import doctest
+import inspect
 import os
 from StringIO import StringIO
 import unittest
@@ -164,6 +165,45 @@
                                                        'foos1', 2))
 
 
+class NullTranslationsTestCase(unittest.TestCase):
+    def setUp(self):
+        fp = StringIO()
+        write_mo(fp, Catalog(locale='de'))
+        fp.seek(0)
+        self.translations = support.Translations(fileobj=fp)
+        self.null_translations = support.NullTranslations(fp=fp)
+    
+    def method_names(self):
+        return [name for name in dir(self.translations) if 'gettext' in name]
+    
+    def test_same_methods(self):
+        for name in self.method_names():
+            if not hasattr(self.null_translations, name):
+                self.fail('NullTranslations does not provide method %r' % name)
+    
+    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)
+            signature = inspect.getargspec
+            self.assertEqual(signature(translations_method), 
+                             signature(null_method))
+    
+    def test_same_return_values(self):
+        data = {
+            'message': u'foo', 'domain': u'domain', 'context': 'tests',
+            'singular': u'bar', 'plural': u'baz', 'num': 1,
+            'msgid1': u'bar', 'msgid2': u'baz', 'n': 1,
+        }
+        for name in self.method_names():
+            method = getattr(self.translations, name)
+            null_method = getattr(self.null_translations, name)
+            signature = inspect.getargspec(method)
+            parameter_names = [name for name in signature.args if name != 'self']
+            values = [data[name] for name in parameter_names]
+            self.assertEqual(method(*values), null_method(*values))
+
+
 class LazyProxyTestCase(unittest.TestCase):
     def test_proxy_caches_result_of_function_call(self):
         self.counter = 0
@@ -188,6 +228,7 @@
     suite = unittest.TestSuite()
     suite.addTest(doctest.DocTestSuite(support))
     suite.addTest(unittest.makeSuite(TranslationsTestCase, 'test'))
+    suite.addTest(unittest.makeSuite(NullTranslationsTestCase, 'test'))
     suite.addTest(unittest.makeSuite(LazyProxyTestCase, 'test'))
     return suite
 
Copyright (C) 2012-2017 Edgewall Software