# HG changeset patch # User fschwarz # Date 1345490482 0 # Node ID f63a07d648b6a3afd231ff0cd4249fcb5011fbcc # Parent 57a08cc52623c465a22283bdb6bc07c6e3820c0d add babel.support.NullTranslations class similar to gettext.NullTranslations but with all of Babel's new *gettext methods (#277) diff --git a/ChangeLog b/ChangeLog --- 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 diff --git a/babel/support.py b/babel/support.py --- 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 + diff --git a/babel/tests/support.py b/babel/tests/support.py --- 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