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 <abokovoy@redhat.com>
This commit is contained in:
Stanislav Levin
2018-06-18 09:49:27 +03:00
committed by Christian Heimes
parent 8c3ff0308c
commit f4716b6991
2 changed files with 97 additions and 0 deletions

View File

@@ -67,6 +67,17 @@ u'Hello, Joe.'
>>> unicode(my_plugin.my_string) % dict(name='Joe') # Long form >>> unicode(my_plugin.my_string) % dict(name='Joe') # Long form
u'Hello, Joe.' 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 Similar to ``_()``, the ``ngettext()`` function above is actually an
`NGettextFactory` instance, which when called returns an `NGettext` instance. `NGettextFactory` instance, which when called returns an `NGettext` instance.
An `NGettext` instance stores the singular and plural messages, and the gettext 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 >>> my_plugin.my_plural(1) % dict(count=1) # Long form
u'1 goose' 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 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 domain. The default domain is ``'ipa'``, which is also the domain of the
standard ``ipalib._()`` and ``ipalib.ngettext()`` factories. But 3rd-party standard ``ipalib._()`` and ``ipalib.ngettext()`` factories. But 3rd-party
@@ -230,6 +250,19 @@ class Gettext(LazyText):
>>> unicode(msg) % dict(name='Joe') # Long form >>> unicode(msg) % dict(name='Joe') # Long form
u'Hello, Joe.' 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 See `GettextFactory` for additional details. If you need to pick between
singular and plural form, use `NGettext` instances via the singular and plural form, use `NGettext` instances via the
`NGettextFactory`. `NGettextFactory`.
@@ -268,6 +301,9 @@ class Gettext(LazyText):
def __mod__(self, kw): def __mod__(self, kw):
return unicode(self) % kw #pylint: disable=no-member return unicode(self) % kw #pylint: disable=no-member
def format(self, *args, **kwargs):
return unicode(self).format(*args, **kwargs)
@six.python_2_unicode_compatible @six.python_2_unicode_compatible
class FixMe(Gettext): class FixMe(Gettext):
@@ -385,6 +421,33 @@ class NGettext(LazyText):
>>> msg2(0) % dict(num=0) >>> msg2(0) % dict(num=0)
u'0 geese' 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. See `NGettextFactory` for additional details.
""" """
@@ -404,6 +467,10 @@ class NGettext(LazyText):
count = kw['count'] count = kw['count']
return self(count) % kw return self(count) % kw
def format(self, *args, **kwargs):
count = kwargs['count']
return self(count).format(*args, **kwargs)
def __call__(self, count): def __call__(self, count):
if self.key in context.__dict__: if self.key in context.__dict__:
t = context.__dict__[self.key] t = context.__dict__[self.key]
@@ -442,6 +509,9 @@ class ConcatenatedLazyText(object):
def __mod__(self, kw): def __mod__(self, kw):
return unicode(self) % kw return unicode(self) % kw
def format(self, *args, **kwargs):
return unicode(self).format(*args, **kwargs)
def __add__(self, other): def __add__(self, other):
if isinstance(other, ConcatenatedLazyText): if isinstance(other, ConcatenatedLazyText):
return ConcatenatedLazyText(*self.components + other.components) return ConcatenatedLazyText(*self.components + other.components)

View File

@@ -201,6 +201,12 @@ class test_Gettext(object):
inst = self.klass('hello %(adj)s nurse', 'foo', 'bar') inst = self.klass('hello %(adj)s nurse', 'foo', 'bar')
assert inst % dict(adj='tall', stuff='junk') == 'hello tall nurse' 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): def test_eq(self):
inst1 = self.klass('what up?', 'foo', 'bar') inst1 = self.klass('what up?', 'foo', 'bar')
inst2 = 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=1, dish='stew') == '1 goose makes a stew'
assert inst % dict(count=2, dish='pie') == '2 geese make a pie' 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): def test_eq(self):
inst1 = self.klass(singular, plural, 'foo', 'bar') inst1 = self.klass(singular, plural, 'foo', 'bar')
inst2 = 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'), ']') inst = self.klass('[', text.Gettext('%(color)s', 'foo', 'bar'), ']')
assert inst % dict(color='red', stuff='junk') == '[red]' 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): def test_add(self):
inst = (text.Gettext('pale ', 'foo', 'bar') + inst = (text.Gettext('pale ', 'foo', 'bar') +
text.Gettext('blue', 'foo', 'bar')) text.Gettext('blue', 'foo', 'bar'))