From f4716b69910f082a2fe039338f4268d941792258 Mon Sep 17 00:00:00 2001 From: Stanislav Levin Date: Jun 21 2018 13:30:58 +0000 Subject: Add support for format method to translation objects For now translation classes have old style % formatting way only. But 'format' is convenience, preferred in Python3 string formatting method. Fixes: https://pagure.io/freeipa/issue/7586 Reviewed-By: Alexander Bokovoy --- diff --git a/ipalib/text.py b/ipalib/text.py index c67479a..51b4546 100644 --- a/ipalib/text.py +++ b/ipalib/text.py @@ -67,6 +67,17 @@ u'Hello, Joe.' >>> unicode(my_plugin.my_string) % dict(name='Joe') # Long form u'Hello, Joe.' +Translation can also be performed via the `Gettext.format()` convenience +method. For example, these two are equivalent: + +>>> my_plugin.my_string = _('Hello, {name}.') +>>> my_plugin.my_string.format(name='Joe') +u'Hello, Joe.' + +>>> my_plugin.my_string = _('Hello, {0}.') +>>> my_plugin.my_string.format('Joe') +u'Hello, Joe.' + Similar to ``_()``, the ``ngettext()`` function above is actually an `NGettextFactory` instance, which when called returns an `NGettext` instance. An `NGettext` instance stores the singular and plural messages, and the gettext @@ -88,6 +99,15 @@ u'1 goose' >>> my_plugin.my_plural(1) % dict(count=1) # Long form u'1 goose' +Translation can also be performed via the `NGettext.format()` convenience +method. For example: + +>>> my_plugin.my_plural = ngettext('{count} goose', '{count} geese', 0) +>>> my_plugin.my_plural.format(count=1) +u'1 goose' +>>> my_plugin.my_plural.format(count=2) +u'2 geese' + Lastly, 3rd-party plugins can create factories bound to a different gettext domain. The default domain is ``'ipa'``, which is also the domain of the standard ``ipalib._()`` and ``ipalib.ngettext()`` factories. But 3rd-party @@ -230,6 +250,19 @@ class Gettext(LazyText): >>> unicode(msg) % dict(name='Joe') # Long form u'Hello, Joe.' + `Gettext.format()` is a convenience method for Python string formatting. + It will translate your message using `Gettext.__unicode__()` and then + perform the string substitution on the translated message. For example, + these two are equivalent: + + >>> msg = Gettext('Hello, {name}.') + >>> msg.format(name='Joe') + u'Hello, Joe.' + + >>> msg = Gettext('Hello, {0}.') + >>> msg.format('Joe') + u'Hello, Joe.' + See `GettextFactory` for additional details. If you need to pick between singular and plural form, use `NGettext` instances via the `NGettextFactory`. @@ -268,6 +301,9 @@ class Gettext(LazyText): def __mod__(self, kw): return unicode(self) % kw #pylint: disable=no-member + def format(self, *args, **kwargs): + return unicode(self).format(*args, **kwargs) + @six.python_2_unicode_compatible class FixMe(Gettext): @@ -385,6 +421,33 @@ class NGettext(LazyText): >>> msg2(0) % dict(num=0) u'0 geese' + `NGettext.format()` is a convenience method for Python string formatting. + It can only be used if your substitution ``dict`` contains the count in a + ``'count'`` item. For example: + + >>> msg = NGettext('{count} goose', '{count} geese') + >>> msg.format(count=0) + u'0 geese' + >>> msg.format(count=1) + u'1 goose' + >>> msg.format(count=2) + u'2 geese' + + A ``KeyError`` is raised if your substitution ``dict`` doesn't have a + ``'count'`` item. For example: + + >>> msg2 = NGettext('{num} goose', '{num} geese') + >>> msg2.format(num=0) + Traceback (most recent call last): + ... + KeyError: 'count' + + However, in this case you can still use the longer, explicit form for + string substitution: + + >>> msg2(0).format(num=0) + u'0 geese' + See `NGettextFactory` for additional details. """ @@ -404,6 +467,10 @@ class NGettext(LazyText): count = kw['count'] return self(count) % kw + def format(self, *args, **kwargs): + count = kwargs['count'] + return self(count).format(*args, **kwargs) + def __call__(self, count): if self.key in context.__dict__: t = context.__dict__[self.key] @@ -442,6 +509,9 @@ class ConcatenatedLazyText(object): def __mod__(self, kw): return unicode(self) % kw + def format(self, *args, **kwargs): + return unicode(self).format(*args, **kwargs) + def __add__(self, other): if isinstance(other, ConcatenatedLazyText): return ConcatenatedLazyText(*self.components + other.components) diff --git a/ipatests/test_ipalib/test_text.py b/ipatests/test_ipalib/test_text.py index 05b5dd3..d09adec 100644 --- a/ipatests/test_ipalib/test_text.py +++ b/ipatests/test_ipalib/test_text.py @@ -201,6 +201,12 @@ class test_Gettext(object): inst = self.klass('hello %(adj)s nurse', 'foo', 'bar') assert inst % dict(adj='tall', stuff='junk') == 'hello tall nurse' + def test_format(self): + inst = self.klass('{0} {adj} nurse', 'foo', 'bar') + posargs = ('hello', 'bye') + knownargs = {'adj': 'caring', 'stuff': 'junk'} + assert inst.format(*posargs, **knownargs) == 'hello caring nurse' + def test_eq(self): inst1 = self.klass('what up?', 'foo', 'bar') inst2 = self.klass('what up?', 'foo', 'bar') @@ -264,6 +270,21 @@ class test_NGettext(object): assert inst % dict(count=1, dish='stew') == '1 goose makes a stew' assert inst % dict(count=2, dish='pie') == '2 geese make a pie' + def test_format(self): + singular = '{count} goose makes a {0} {dish}' + plural = '{count} geese make a {0} {dish}' + inst = self.klass(singular, plural, 'foo', 'bar') + posargs = ('tasty', 'disgusting') + knownargs0 = {'count': 0, 'dish': 'frown', 'stuff': 'junk'} + knownargs1 = {'count': 1, 'dish': 'stew', 'stuff': 'junk'} + knownargs2 = {'count': 2, 'dish': 'pie', 'stuff': 'junk'} + expected_str0 = '0 geese make a tasty frown' + expected_str1 = '1 goose makes a tasty stew' + expected_str2 = '2 geese make a tasty pie' + assert inst.format(*posargs, **knownargs0) == expected_str0 + assert inst.format(*posargs, **knownargs1) == expected_str1 + assert inst.format(*posargs, **knownargs2) == expected_str2 + def test_eq(self): inst1 = self.klass(singular, plural, 'foo', 'bar') inst2 = self.klass(singular, plural, 'foo', 'bar') @@ -387,6 +408,12 @@ class test_ConcatenatedText(object): inst = self.klass('[', text.Gettext('%(color)s', 'foo', 'bar'), ']') assert inst % dict(color='red', stuff='junk') == '[red]' + def test_format(self): + inst = self.klass('{0}', text.Gettext('{color}', 'foo', 'bar'), ']') + posargs = ('[', '(') + knownargs = {'color': 'red', 'stuff': 'junk'} + assert inst.format(*posargs, **knownargs) == '[red]' + def test_add(self): inst = (text.Gettext('pale ', 'foo', 'bar') + text.Gettext('blue', 'foo', 'bar'))