Edgewall Software

Ticket #281: improve_param_error_handling.patch

File improve_param_error_handling.patch, 21.4 KB (added by palgarvio, 3 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):