Edgewall Software

Ticket #281: improve_param_error_handling.patch

File improve_param_error_handling.patch, 21.4 KB (added by palgarvio, 14 years ago)
  • genshi/filters/i18n.py

    diff --git a/genshi/filters/i18n.py b/genshi/filters/i18n.py
    a b  
    5151class I18NDirectiveExtract(I18NDirective):
    5252    """Simple interface for directives to support messages extraction"""
    5353
    54     def extract(self, stream, ctxt):
     54    def extract(self, stream, ctxt, error_callback=None):
    5555        raise NotImplementedError
    5656
    5757
    5858class CommentDirective(I18NDirective):
    5959    """Implementation of the ``i18n:comment`` template directive which adds
    6060    translation comments.
    61    
     61
    6262    >>> from genshi.template import MarkupTemplate
    6363    >>>
    6464    >>> tmpl = MarkupTemplate('''<html xmlns:i18n="http://genshi.edgewall.org/i18n">
     
    8383class MsgDirective(I18NDirectiveExtract):
    8484    r"""Implementation of the ``i18n:msg`` directive which marks inner content
    8585    as translatable. Consider the following examples:
    86    
     86
    8787    >>> from genshi.template import MarkupTemplate
    8888    >>> tmpl = MarkupTemplate('''<html xmlns:i18n="http://genshi.edgewall.org/i18n">
    8989    ...   <div i18n:msg="">
     
    9292    ...   </div>
    9393    ...   <p i18n:msg="">Foo <em>bar</em>!</p>
    9494    ... </html>''')
    95    
     95
    9696    >>> translator = Translator()
    9797    >>> translator.setup(tmpl)
    9898    >>> list(translator.extract(tmpl.stream))
     
    131131      <p>Foo <em>bar</em>!</p>
    132132    </html>
    133133    >>>
    134    
     134
    135135    Starting and ending white-space is stripped of to make it simpler for
    136136    translators. Stripping it is not that important since it's on the html
    137137    source, the rendered output will remain the same.
     
    179179
    180180        return _apply_directives(_generate(), directives, ctxt)
    181181
    182     def extract(self, stream, ctxt):
    183         msgbuf = MessageBuffer(self)
     182    def extract(self, stream, ctxt, error_callback=None):
     183        msgbuf = MessageBuffer(self, error_callback=error_callback)
    184184
    185185        stream = iter(stream)
    186186        previous = stream.next()
     
    192192        if previous[0] is not END:
    193193            msgbuf.append(*previous)
    194194
    195         yield None, msgbuf.format(), filter(None, [ctxt.get('_i18n.comment')])
     195        if msgbuf.valid:
     196            yield (None, msgbuf.format(),
     197                   filter(None, [ctxt.get('_i18n.comment')]))
    196198
    197199
    198200class InnerChooseDirective(I18NDirective):
     
    241243class ChooseDirective(I18NDirectiveExtract):
    242244    """Implementation of the ``i18n:choose`` directive which provides plural
    243245    internationalisation of strings.
    244    
     246
    245247    This directive requires at least one parameter, the one which evaluates to
    246248    an integer which will allow to choose the plural/singular form. If you also
    247249    have expressions inside the singular and plural version of the string you
    248250    also need to pass a name for those parameters. Consider the following
    249251    examples:
    250    
     252
    251253    >>> from genshi.template import MarkupTemplate
    252254    >>>
    253255    >>> translator = Translator()
     
    285287      </div>
    286288    </html>
    287289    >>>
    288    
     290
    289291    When used as a directive and not as an attribute:
    290292    >>> tmpl = MarkupTemplate('''\
    291293        <html xmlns:i18n="http://genshi.edgewall.org/i18n">
     
    381383
    382384        ctxt.pop()
    383385
    384     def extract(self, stream, ctxt):
     386    def extract(self, stream, ctxt, error_callback=None):
    385387
    386388        stream = iter(stream)
    387389        previous = stream.next()
    388390        if previous is START:
    389391            stream.next()
    390392
    391         singular_msgbuf = MessageBuffer(self)
    392         plural_msgbuf = MessageBuffer(self)
     393        singular_msgbuf = MessageBuffer(self, error_callback=error_callback)
     394        plural_msgbuf = MessageBuffer(self, error_callback=error_callback)
    393395
    394396        for kind, event, pos in stream:
    395397            if kind is SUB:
     
    407409            else:
    408410                singular_msgbuf.append(kind, event, pos)
    409411                plural_msgbuf.append(kind, event, pos)
    410         yield 'ngettext', \
    411             (singular_msgbuf.format(), plural_msgbuf.format()), \
    412             filter(None, [ctxt.get('_i18n.comment')])
     412        if singular_msgbuf.valid and plural_msgbuf.valid:
     413            yield 'ngettext', \
     414                (singular_msgbuf.format(), plural_msgbuf.format()), \
     415                filter(None, [ctxt.get('_i18n.comment')])
    413416
    414417
    415418class DomainDirective(I18NDirective):
    416419    """Implementation of the ``i18n:domain`` directive which allows choosing
    417420    another i18n domain(catalog) to translate from.
    418    
     421
    419422    >>> from genshi.template.markup import MarkupTemplate
    420423
    421424    >>> class DummyTranslations(NullTranslations):
     
    479482    def __init__(self, value, template, hints=None, namespaces=None,
    480483                 lineno=-1, offset=-1):
    481484        Directive.__init__(self, None, template, namespaces, lineno, offset)
    482         self.domain = value and value.strip() or '__DEFAULT__' 
     485        self.domain = value and value.strip() or '__DEFAULT__'
    483486
    484487    @classmethod
    485488    def attach(cls, template, stream, value, namespaces, pos):
     
    498501class Translator(DirectiveFactory):
    499502    """Can extract and translate localizable strings from markup streams and
    500503    templates.
    501    
     504
    502505    For example, assume the following template:
    503    
     506
    504507    >>> from genshi.template import MarkupTemplate
    505508    >>>
    506509    >>> tmpl = MarkupTemplate('''<html xmlns:py="http://genshi.edgewall.org/">
     
    512515    ...     <p>${_("Hello, %(name)s") % dict(name=username)}</p>
    513516    ...   </body>
    514517    ... </html>''', filename='example.html')
    515    
     518
    516519    For demonstration, we define a dummy ``gettext``-style function with a
    517520    hard-coded translation table, and pass that to the `Translator` initializer:
    518    
     521
    519522    >>> def pseudo_gettext(string):
    520523    ...     return {
    521524    ...         'Example': 'Beispiel',
     
    523526    ...     }[string]
    524527    >>>
    525528    >>> translator = Translator(pseudo_gettext)
    526    
     529
    527530    Next, the translator needs to be prepended to any already defined filters
    528531    on the template:
    529    
     532
    530533    >>> tmpl.filters.insert(0, translator)
    531    
     534
    532535    When generating the template output, our hard-coded translations should be
    533536    applied as expected:
    534    
     537
    535538    >>> print tmpl.generate(username='Hans', _=pseudo_gettext)
    536539    <html>
    537540      <head>
     
    542545        <p>Hallo, Hans</p>
    543546      </body>
    544547    </html>
    545    
     548
    546549    Note that elements defining ``xml:lang`` attributes that do not contain
    547550    variable expressions are ignored by this filter. That can be used to
    548551    exclude specific parts of a template from being extracted and translated.
     
    569572    def __init__(self, translate=NullTranslations(), ignore_tags=IGNORE_TAGS,
    570573                 include_attrs=INCLUDE_ATTRS, extract_text=True):
    571574        """Initialize the translator.
    572        
     575
    573576        :param translate: the translation function, for example ``gettext`` or
    574577                          ``ugettext``.
    575578        :param ignore_tags: a set of tag names that should not be localized
     
    577580        :param extract_text: whether the content of text nodes should be
    578581                             extracted, or only text in explicit ``gettext``
    579582                             function calls
    580        
     583
    581584        :note: Changed in 0.6: the `translate` parameter can now be either
    582585               a ``gettext``-style function, or an object compatible with the
    583586               ``NullTransalations`` or ``GNUTranslations`` interface
     
    589592
    590593    def __call__(self, stream, ctxt=None, search_text=True):
    591594        """Translate any localizable strings in the given stream.
    592        
     595
    593596        This function shouldn't be called directly. Instead, an instance of
    594597        the `Translator` class should be registered as a filter with the
    595598        `Template` or the `TemplateLoader`, or applied as a regular stream
    596599        filter. If used as a template filter, it should be inserted in front of
    597600        all the default filters.
    598        
     601
    599602        :param stream: the markup event stream
    600603        :param ctxt: the template context (not used)
    601604        :param search_text: whether text nodes should be translated (used
     
    720723                         'ugettext', 'ungettext')
    721724
    722725    def extract(self, stream, gettext_functions=GETTEXT_FUNCTIONS,
    723                 search_text=True, msgbuf=None, ctxt=Context()):
     726                search_text=True, msgbuf=None, ctxt=Context(),
     727                error_callback=None):
    724728        """Extract localizable strings from the given template stream.
    725        
     729
    726730        For every string found, this function yields a ``(lineno, function,
    727731        message, comments)`` tuple, where:
    728        
     732
    729733        * ``lineno`` is the number of the line on which the string was found,
    730734        * ``function`` is the name of the ``gettext`` function used (if the
    731735          string was extracted from embedded Python code), and
     
    734738           arguments).
    735739        *  ``comments`` is a list of comments related to the message, extracted
    736740           from ``i18n:comment`` attributes found in the markup
    737        
     741
    738742        >>> from genshi.template import MarkupTemplate
    739743        >>>
    740744        >>> tmpl = MarkupTemplate('''<html xmlns:py="http://genshi.edgewall.org/">
     
    754758        6, None, u'Example'
    755759        7, '_', u'Hello, %(name)s'
    756760        8, 'ngettext', (u'You have %d item', u'You have %d items', None)
    757        
     761
    758762        :param stream: the event stream to extract strings from; can be a
    759763                       regular stream or a template stream
    760764        :param gettext_functions: a sequence of function names that should be
     
    763767        :param search_text: whether the content of text nodes should be
    764768                            extracted (used internally)
    765769        :param ctxt: the current extraction context (used internaly)
    766        
     770        :param error_callback: a function that gets called in case of extraction
     771                               errors with the following arguments:
     772                                 (filename, line_number, error_message)
     773
    767774        :note: Changed in 0.4.1: For a function with multiple string arguments
    768775               (such as ``ngettext``), a single item with a tuple of strings is
    769776               yielded, instead an item for each string argument.
    770777        :note: Changed in 0.6: The returned tuples now include a 4th element,
    771778               which is a list of comments for the translator. Added an ``ctxt``
    772779               argument which is used to pass arround the current extraction
    773                context.
     780               context. Added an error_callback used to show errors to the user.
    774781        """
    775782        if not self.extract_text:
    776783            search_text = False
     
    803810                                yield pos[1], None, text, []
    804811                    else:
    805812                        for lineno, funcname, text, comments in self.extract(
    806                                 _ensure(value), gettext_functions,
    807                                 search_text=False):
     813                                            _ensure(value), gettext_functions,
     814                                            search_text=False,
     815                                            error_callback=error_callback):
    808816                            yield lineno, funcname, text, comments
    809817
    810818                if msgbuf:
     
    821829
    822830            elif not skip and msgbuf and kind is END:
    823831                msgbuf.append(kind, data, pos)
    824                 if not msgbuf.depth:
     832                if not msgbuf.depth and msgbuf.valid:
    825833                    yield msgbuf.lineno, None, msgbuf.format(), \
    826834                                                  filter(None, [msgbuf.comment])
    827835                    msgbuf = None
     
    850858                            messages = self.extract(
    851859                                substream, gettext_functions,
    852860                                search_text=search_text and not skip,
    853                                 msgbuf=msgbuf, ctxt=ctxt)
     861                                msgbuf=msgbuf, ctxt=ctxt,
     862                                error_callback=error_callback)
    854863                            for lineno, funcname, text, comments in messages:
    855864                                yield lineno, funcname, text, comments
    856865                        directives.pop(idx)
     
    870879
    871880                for directive in directives:
    872881                    if isinstance(directive, I18NDirectiveExtract):
    873                         messages = directive.extract(substream, ctxt)
     882                        messages = directive.extract(
     883                                substream, ctxt, error_callback=error_callback
     884                        )
    874885                        for funcname, text, comments in messages:
    875886                            yield pos[1], funcname, text, comments
    876887                    else:
    877888                        messages = self.extract(
    878889                            substream, gettext_functions,
    879                             search_text=search_text and not skip, msgbuf=msgbuf)
     890                            search_text=search_text and not skip, msgbuf=msgbuf,
     891                            error_callback=error_callback)
    880892                        for lineno, funcname, text, comments in messages:
    881893                            yield lineno, funcname, text, comments
    882894                if comment:
     
    885897    def setup(self, template):
    886898        """Convenience function to register the `Translator` filter and the
    887899        related directives with the given template.
    888        
     900
    889901        :param template: a `Template` instance
    890902        """
    891903        template.filters.insert(0, self)
     
    895907
    896908class MessageBuffer(object):
    897909    """Helper class for managing internationalized mixed content.
    898    
     910
    899911    :since: version 0.5
    900912    """
    901913
    902     def __init__(self, directive=None):
     914    def __init__(self, directive=None, error_callback=None):
    903915        """Initialize the message buffer.
    904        
     916
    905917        :param params: comma-separated list of parameter names
    906918        :type params: `basestring`
    907919        :param lineno: the line number on which the first stream event
     
    917929        self.depth = 1
    918930        self.order = 1
    919931        self.stack = [0]
     932        self.error_callback = error_callback
     933        self.valid = True
    920934        self.subdirectives = {}
    921935
    922936    def append(self, kind, data, pos):
    923937        """Append a stream event to the buffer.
    924        
     938
    925939        :param kind: the stream event kind
    926940        :param data: the event data
    927941        :param pos: the position of the event in the source
    928942        """
    929         if kind is SUB:
     943        if not self.valid:
     944            # Buffer is already invalid, no point wasting process time here
     945            return
     946        elif kind is SUB:
    930947            # The order needs to be +1 because a new START kind event will
    931948            # happen and we we need to wrap those events into our custom kind(s)
    932949            order = self.stack[-1] + 1
     
    948965            if self.params:
    949966                param = self.params.pop(0)
    950967            else:
    951                 params = ', '.join(['"%s"' % p for p in self.orig_params if p])
    952                 if params:
    953                     params = "(%s)" % params
    954                 raise IndexError("%d parameters%s given to 'i18n:%s' but "
    955                                  "%d or more expressions used in '%s', line %s"
    956                                  % (len(self.orig_params), params,
    957                                     self.directive.tagname,
    958                                     len(self.orig_params)+1,
    959                                     os.path.basename(pos[0] or
    960                                                      'In Memmory Template'),
    961                                     pos[1]))
     968                # Failed to get the expression param name
     969                # Invalidate and if possible warn
     970                self.valid = False
     971                if self.error_callback:
     972                    params = ', '.join(['"%s"' % p for p in
     973                                        self.orig_params if p])
     974                    fname, lineno = pos[0] or '(unknown)', pos[1]
     975                    if params:
     976                        params = "(%s)" % params
     977                    self.error_callback(
     978                        fname, lineno,
     979                        "%d parameters%s given to 'i18n:%s' but %d or more "
     980                        "expressions used. Stopped Processing File."
     981                        % (len(self.orig_params), params,
     982                           self.directive.tagname, len(self.orig_params)+1)
     983                    )
     984                # Nothing else to do here
     985                return
    962986            self.string.append('%%(%s)s' % param)
    963987            self.events.setdefault(self.stack[-1], []).append((kind, data, pos))
    964988            self.values[param] = (kind, data, pos)
    965989        else:
    966             if kind is START: 
     990            if kind is START:
    967991                self.string.append(u'[%d:' % self.order)
    968992                self.stack.append(self.order)
    969993                self.events.setdefault(self.stack[-1],
     
    9861010    def translate(self, string, regex=re.compile(r'%\((\w+)\)s')):
    9871011        """Interpolate the given message translation with the events in the
    9881012        buffer and return the translated stream.
    989        
     1013
    9901014        :param string: the translated message string
    9911015        """
    9921016        substream = None
    993        
     1017
    9941018        def yield_parts(string):
    9951019            for idx, part in enumerate(regex.split(string)):
    9961020                if idx % 2:
     
    10771101def parse_msg(string, regex=re.compile(r'(?:\[(\d+)\:)|(?<!\\)\]')):
    10781102    """Parse a translated message using Genshi mixed content message
    10791103    formatting.
    1080    
     1104
    10811105    >>> parse_msg("See [1:Help].")
    10821106    [(0, 'See '), (1, 'Help'), (0, '.')]
    1083    
     1107
    10841108    >>> parse_msg("See [1:our [2:Help] page] for details.")
    10851109    [(0, 'See '), (1, 'our '), (2, 'Help'), (1, ' page'), (0, ' for details.')]
    1086    
     1110
    10871111    >>> parse_msg("[2:Details] finden Sie in [1:Hilfe].")
    10881112    [(2, 'Details'), (0, ' finden Sie in '), (1, 'Hilfe'), (0, '.')]
    1089    
     1113
    10901114    >>> parse_msg("[1:] Bilder pro Seite anzeigen.")
    10911115    [(1, ''), (0, ' Bilder pro Seite anzeigen.')]
    1092    
     1116
    10931117    :param string: the translated message string
    10941118    :return: a list of ``(order, string)`` tuples
    10951119    :rtype: `list`
     
    11211145
    11221146def extract_from_code(code, gettext_functions):
    11231147    """Extract strings from Python bytecode.
    1124    
     1148
    11251149    >>> from genshi.template.eval import Expression
    1126    
     1150
    11271151    >>> expr = Expression('_("Hello")')
    11281152    >>> list(extract_from_code(expr, Translator.GETTEXT_FUNCTIONS))
    11291153    [('_', u'Hello')]
    1130    
     1154
    11311155    >>> expr = Expression('ngettext("You have %(num)s item", '
    11321156    ...                            '"You have %(num)s items", num)')
    11331157    >>> list(extract_from_code(expr, Translator.GETTEXT_FUNCTIONS))
    11341158    [('ngettext', (u'You have %(num)s item', u'You have %(num)s items', None))]
    1135    
     1159
    11361160    :param code: the `Code` object
    11371161    :type code: `genshi.template.eval.Code`
    11381162    :param gettext_functions: a sequence of function names
     
    11721196
    11731197def extract(fileobj, keywords, comment_tags, options):
    11741198    """Babel extraction method for Genshi templates.
    1175    
     1199
    11761200    :param fileobj: the file-like object the messages should be extracted from
    11771201    :param keywords: a list of keywords (i.e. function names) that should be
    11781202                     recognized as translation functions
     
    12051229    tmpl = template_class(fileobj, filename=getattr(fileobj, 'name', None),
    12061230                          encoding=encoding)
    12071231
     1232    error_callback = options.get('error_callback')
     1233
    12081234    translator = Translator(None, ignore_tags, include_attrs, extract_text)
    12091235    if hasattr(tmpl, 'add_directives'):
    12101236        tmpl.add_directives(Translator.NAMESPACE, translator)
    1211     for message in translator.extract(tmpl.stream, gettext_functions=keywords):
     1237    for message in translator.extract(tmpl.stream, gettext_functions=keywords,
     1238                                      error_callback=error_callback):
    12121239        yield message
    12131240
    12141241
  • genshi/filters/tests/i18n.py

    diff --git a/genshi/filters/tests/i18n.py b/genshi/filters/tests/i18n.py
    a b  
    14271427          <p>BarFoo</p>
    14281428        """, tmpl.generate().render())
    14291429
     1430    def test_translator_error_callback(self):
     1431
     1432        self.test_translator_error_callback_error_called = False
     1433        self.test_translator_error_callback_error_msg = ''
     1434
     1435        def error_callback(filename, line_number, error_message):
     1436            self.test_translator_error_callback_error_called = True
     1437            self.test_translator_error_callback_error_msg = (
     1438                "Error '%s' in filename %s line number %d." % (
     1439                error_message, filename, line_number
     1440            ))
     1441
     1442        tmpl = MarkupTemplate("""<html xmlns:py="http://genshi.edgewall.org/"
     1443            xmlns:i18n="http://genshi.edgewall.org/i18n">
     1444          <p i18n:msg="" py:strip="">
     1445            Please see ${ foo } <a href="help.html">Help</a> for details.
     1446          </p>
     1447        </html>""")
     1448        translator = Translator()
     1449        translator.setup(tmpl)
     1450
     1451        list(translator.extract(tmpl.stream, error_callback=error_callback))
     1452
     1453        self.assertEqual(
     1454            self.test_translator_error_callback_error_msg,
     1455            "Error '0 parameters given to 'i18n:msg' but 1 or more expressions "
     1456            "used. Stopped Processing File.' in filename (unknown) "
     1457            "line number 4.")
     1458        self.assertEqual(self.test_translator_error_callback_error_called, True)
     1459        del self.test_translator_error_callback_error_called
     1460        del self.test_translator_error_callback_error_msg
     1461
    14301462class ExtractTestCase(unittest.TestCase):
    14311463
    14321464    def test_markup_template_extraction(self):