diff babel/messages/checkers.py @ 352:8860097a9765 trunk

The builtin checkers don't require setuptools any longer, validate_format and python_format from the checkers module are merged into one now.
author aronacher
date Tue, 17 Jun 2008 19:54:34 +0000
parents 38053412171b
children 0791b3bf42cc
line wrap: on
line diff
--- a/babel/messages/checkers.py
+++ b/babel/messages/checkers.py
@@ -16,8 +16,17 @@
 :since: version 0.9
 """
 
+from itertools import izip
 from babel.messages.catalog import TranslationError, PYTHON_FORMAT
 
+#: list of format chars that are compatible to each other
+_string_format_compatibilities = [
+    set(['i', 'd', 'u']),
+    set(['x', 'X']),
+    set(['f', 'F', 'g', 'G'])
+]
+
+
 def num_plurals(catalog, message):
     """Verify the number of plurals in the translation."""
     if not message.pluralizable:
@@ -33,19 +42,125 @@
         raise TranslationError("Wrong number of plural forms (expected %d)" %
                                catalog.num_plurals)
 
+
 def python_format(catalog, message):
-    if 'python-format' in message.flags:
-        msgids = message.id
-        if not isinstance(msgids, (list, tuple)):
-            msgids = (msgids,)
-        msgstrs = message.string
-        if not isinstance(msgstrs, (list, tuple)):
-            msgstrs = (msgstrs,)
-        for idx, msgid in enumerate(msgids):
-            if not msgstrs[idx]:
-                continue # no translation
-            for match in PYTHON_FORMAT.finditer(msgid):
-                param = match.group(0)
-                if param not in msgstrs[idx]:
-                    raise TranslationError("Python parameter %s not found in "
-                                           "translation" % param)
+    if 'python-format' not in message.flags:
+        return
+    msgids = message.id
+    if not isinstance(msgids, (list, tuple)):
+        msgids = (msgids,)
+    msgstrs = message.string
+    if not isinstance(msgstrs, (list, tuple)):
+        msgstrs = (msgstrs,)
+
+    for msgid, msgstr in izip(msgids, msgstrs):
+        if msgstr:
+            _validate_format(msgid, msgstr)
+
+
+def _validate_format(format, alternative):
+    """Test format string `alternative` against `format`.  `format` can be the
+    msgid of a message and `alternative` one of the `msgstr`\s.  The two
+    arguments are not interchangeable as `alternative` may contain less
+    placeholders if `format` uses named placeholders.
+
+    If `format` does not use string formatting a `ValueError` is raised.
+
+    If the string formatting of `alternative` is compatible to `format` the
+    function returns `None`, otherwise a `TranslationError` is raised.
+
+    Examples for compatible format strings:
+
+    >>> _validate_format('Hello %s!', 'Hallo %s!')
+    >>> _validate_format('Hello %i!', 'Hallo %d!')
+
+    Example for an incompatible format strings:
+
+    >>> _validate_format('Hello %(name)s!', 'Hallo %s!')
+    Traceback (most recent call last):
+      ...
+    TranslationError: the format strings are of different kinds
+
+    This function is used by the `python_format` checker.
+
+    :param format: The original format string
+    :param alternative: The alternative format string that should be checked
+                        against format
+    :return: None on success
+    :raises TranslationError: on formatting errors
+    """
+
+    def _parse(string):
+        result = []
+        for match in PYTHON_FORMAT.finditer(string):
+            name, format, typechar = match.groups()
+            if typechar == '%' and name is not None:
+                continue
+            result.append((name, typechar))
+        return result
+
+    def _compatible(a, b):
+        if a == b:
+            return True
+        for set in _string_format_compatibilities:
+            if a in set and b in set:
+                return True
+        return False
+
+    def _check_positional(results):
+        positional = None
+        for name, char in results:
+            if positional is None:
+                positional = name is None
+            else:
+                if (name is None) != positional:
+                    raise ValueError('format string mixes positional '
+                                     'and named placeholders')
+        return bool(positional)
+
+    a, b = map(_parse, (format, alternative))
+
+    # if a does not use string formattings, we are dealing with invalid
+    # input data.  This function only works if the first string provided
+    # does contain string format chars
+    if not a:
+        raise ValueError('original string provided does not use string '
+                         'formatting.')
+
+    # now check if both strings are positional or named
+    a_positional, b_positional = map(_check_positional, (a, b))
+    if a_positional and not b_positional and not b:
+        raise TranslationError('placeholders are incompatible')
+    elif a_positional != b_positional:
+        raise TranslationError('the format strings are of different kinds')
+
+    # if we are operating on positional strings both must have the
+    # same number of format chars and those must be compatible
+    if a_positional:
+        if len(a) != len(b):
+            raise TranslationError('positional format placeholders are '
+                                   'unbalanced')
+        for idx, ((_, first), (_, second)) in enumerate(izip(a, b)):
+            if not _compatible(first, second):
+                raise TranslationError('incompatible format for placeholder '
+                                       '%d: %r and %r are not compatible' %
+                                       (idx + 1, first, second))
+
+    # otherwise the second string must not have names the first one
+    # doesn't have and the types of those included must be compatible
+    else:
+        type_map = dict(a)
+        for name, typechar in b:
+            if name not in type_map:
+                raise TranslationError('unknown named placeholder %r' % name)
+            elif not _compatible(typechar, type_map[name]):
+                raise TranslationErrorError('incompatible format for '
+                                            'placeholder %r: '
+                                            '%r and %r are not compatible' %
+                                            (name, typechar, type_map[name]))
+
+
+#: list of builtin checkers for babel installations without setuptools.
+#: Keep this in sync with the mapping in the setup.py
+#: :see: babel.messages.catalog.Catalog.check
+builtin_checkers = [num_plurals, python_format]
Copyright (C) 2012-2017 Edgewall Software