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 51 51 class I18NDirectiveExtract(I18NDirective): 52 52 """Simple interface for directives to support messages extraction""" 53 53 54 def extract(self, stream, ctxt ):54 def extract(self, stream, ctxt, error_callback=None): 55 55 raise NotImplementedError 56 56 57 57 58 58 class CommentDirective(I18NDirective): 59 59 """Implementation of the ``i18n:comment`` template directive which adds 60 60 translation comments. 61 61 62 62 >>> from genshi.template import MarkupTemplate 63 63 >>> 64 64 >>> tmpl = MarkupTemplate('''<html xmlns:i18n="http://genshi.edgewall.org/i18n"> … … 83 83 class MsgDirective(I18NDirectiveExtract): 84 84 r"""Implementation of the ``i18n:msg`` directive which marks inner content 85 85 as translatable. Consider the following examples: 86 86 87 87 >>> from genshi.template import MarkupTemplate 88 88 >>> tmpl = MarkupTemplate('''<html xmlns:i18n="http://genshi.edgewall.org/i18n"> 89 89 ... <div i18n:msg=""> … … 92 92 ... </div> 93 93 ... <p i18n:msg="">Foo <em>bar</em>!</p> 94 94 ... </html>''') 95 95 96 96 >>> translator = Translator() 97 97 >>> translator.setup(tmpl) 98 98 >>> list(translator.extract(tmpl.stream)) … … 131 131 <p>Foo <em>bar</em>!</p> 132 132 </html> 133 133 >>> 134 134 135 135 Starting and ending white-space is stripped of to make it simpler for 136 136 translators. Stripping it is not that important since it's on the html 137 137 source, the rendered output will remain the same. … … 179 179 180 180 return _apply_directives(_generate(), directives, ctxt) 181 181 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) 184 184 185 185 stream = iter(stream) 186 186 previous = stream.next() … … 192 192 if previous[0] is not END: 193 193 msgbuf.append(*previous) 194 194 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')])) 196 198 197 199 198 200 class InnerChooseDirective(I18NDirective): … … 241 243 class ChooseDirective(I18NDirectiveExtract): 242 244 """Implementation of the ``i18n:choose`` directive which provides plural 243 245 internationalisation of strings. 244 246 245 247 This directive requires at least one parameter, the one which evaluates to 246 248 an integer which will allow to choose the plural/singular form. If you also 247 249 have expressions inside the singular and plural version of the string you 248 250 also need to pass a name for those parameters. Consider the following 249 251 examples: 250 252 251 253 >>> from genshi.template import MarkupTemplate 252 254 >>> 253 255 >>> translator = Translator() … … 285 287 </div> 286 288 </html> 287 289 >>> 288 290 289 291 When used as a directive and not as an attribute: 290 292 >>> tmpl = MarkupTemplate('''\ 291 293 <html xmlns:i18n="http://genshi.edgewall.org/i18n"> … … 381 383 382 384 ctxt.pop() 383 385 384 def extract(self, stream, ctxt ):386 def extract(self, stream, ctxt, error_callback=None): 385 387 386 388 stream = iter(stream) 387 389 previous = stream.next() 388 390 if previous is START: 389 391 stream.next() 390 392 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) 393 395 394 396 for kind, event, pos in stream: 395 397 if kind is SUB: … … 407 409 else: 408 410 singular_msgbuf.append(kind, event, pos) 409 411 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')]) 413 416 414 417 415 418 class DomainDirective(I18NDirective): 416 419 """Implementation of the ``i18n:domain`` directive which allows choosing 417 420 another i18n domain(catalog) to translate from. 418 421 419 422 >>> from genshi.template.markup import MarkupTemplate 420 423 421 424 >>> class DummyTranslations(NullTranslations): … … 479 482 def __init__(self, value, template, hints=None, namespaces=None, 480 483 lineno=-1, offset=-1): 481 484 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__' 483 486 484 487 @classmethod 485 488 def attach(cls, template, stream, value, namespaces, pos): … … 498 501 class Translator(DirectiveFactory): 499 502 """Can extract and translate localizable strings from markup streams and 500 503 templates. 501 504 502 505 For example, assume the following template: 503 506 504 507 >>> from genshi.template import MarkupTemplate 505 508 >>> 506 509 >>> tmpl = MarkupTemplate('''<html xmlns:py="http://genshi.edgewall.org/"> … … 512 515 ... <p>${_("Hello, %(name)s") % dict(name=username)}</p> 513 516 ... </body> 514 517 ... </html>''', filename='example.html') 515 518 516 519 For demonstration, we define a dummy ``gettext``-style function with a 517 520 hard-coded translation table, and pass that to the `Translator` initializer: 518 521 519 522 >>> def pseudo_gettext(string): 520 523 ... return { 521 524 ... 'Example': 'Beispiel', … … 523 526 ... }[string] 524 527 >>> 525 528 >>> translator = Translator(pseudo_gettext) 526 529 527 530 Next, the translator needs to be prepended to any already defined filters 528 531 on the template: 529 532 530 533 >>> tmpl.filters.insert(0, translator) 531 534 532 535 When generating the template output, our hard-coded translations should be 533 536 applied as expected: 534 537 535 538 >>> print tmpl.generate(username='Hans', _=pseudo_gettext) 536 539 <html> 537 540 <head> … … 542 545 <p>Hallo, Hans</p> 543 546 </body> 544 547 </html> 545 548 546 549 Note that elements defining ``xml:lang`` attributes that do not contain 547 550 variable expressions are ignored by this filter. That can be used to 548 551 exclude specific parts of a template from being extracted and translated. … … 569 572 def __init__(self, translate=NullTranslations(), ignore_tags=IGNORE_TAGS, 570 573 include_attrs=INCLUDE_ATTRS, extract_text=True): 571 574 """Initialize the translator. 572 575 573 576 :param translate: the translation function, for example ``gettext`` or 574 577 ``ugettext``. 575 578 :param ignore_tags: a set of tag names that should not be localized … … 577 580 :param extract_text: whether the content of text nodes should be 578 581 extracted, or only text in explicit ``gettext`` 579 582 function calls 580 583 581 584 :note: Changed in 0.6: the `translate` parameter can now be either 582 585 a ``gettext``-style function, or an object compatible with the 583 586 ``NullTransalations`` or ``GNUTranslations`` interface … … 589 592 590 593 def __call__(self, stream, ctxt=None, search_text=True): 591 594 """Translate any localizable strings in the given stream. 592 595 593 596 This function shouldn't be called directly. Instead, an instance of 594 597 the `Translator` class should be registered as a filter with the 595 598 `Template` or the `TemplateLoader`, or applied as a regular stream 596 599 filter. If used as a template filter, it should be inserted in front of 597 600 all the default filters. 598 601 599 602 :param stream: the markup event stream 600 603 :param ctxt: the template context (not used) 601 604 :param search_text: whether text nodes should be translated (used … … 720 723 'ugettext', 'ungettext') 721 724 722 725 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): 724 728 """Extract localizable strings from the given template stream. 725 729 726 730 For every string found, this function yields a ``(lineno, function, 727 731 message, comments)`` tuple, where: 728 732 729 733 * ``lineno`` is the number of the line on which the string was found, 730 734 * ``function`` is the name of the ``gettext`` function used (if the 731 735 string was extracted from embedded Python code), and … … 734 738 arguments). 735 739 * ``comments`` is a list of comments related to the message, extracted 736 740 from ``i18n:comment`` attributes found in the markup 737 741 738 742 >>> from genshi.template import MarkupTemplate 739 743 >>> 740 744 >>> tmpl = MarkupTemplate('''<html xmlns:py="http://genshi.edgewall.org/"> … … 754 758 6, None, u'Example' 755 759 7, '_', u'Hello, %(name)s' 756 760 8, 'ngettext', (u'You have %d item', u'You have %d items', None) 757 761 758 762 :param stream: the event stream to extract strings from; can be a 759 763 regular stream or a template stream 760 764 :param gettext_functions: a sequence of function names that should be … … 763 767 :param search_text: whether the content of text nodes should be 764 768 extracted (used internally) 765 769 :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 767 774 :note: Changed in 0.4.1: For a function with multiple string arguments 768 775 (such as ``ngettext``), a single item with a tuple of strings is 769 776 yielded, instead an item for each string argument. 770 777 :note: Changed in 0.6: The returned tuples now include a 4th element, 771 778 which is a list of comments for the translator. Added an ``ctxt`` 772 779 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. 774 781 """ 775 782 if not self.extract_text: 776 783 search_text = False … … 803 810 yield pos[1], None, text, [] 804 811 else: 805 812 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): 808 816 yield lineno, funcname, text, comments 809 817 810 818 if msgbuf: … … 821 829 822 830 elif not skip and msgbuf and kind is END: 823 831 msgbuf.append(kind, data, pos) 824 if not msgbuf.depth :832 if not msgbuf.depth and msgbuf.valid: 825 833 yield msgbuf.lineno, None, msgbuf.format(), \ 826 834 filter(None, [msgbuf.comment]) 827 835 msgbuf = None … … 850 858 messages = self.extract( 851 859 substream, gettext_functions, 852 860 search_text=search_text and not skip, 853 msgbuf=msgbuf, ctxt=ctxt) 861 msgbuf=msgbuf, ctxt=ctxt, 862 error_callback=error_callback) 854 863 for lineno, funcname, text, comments in messages: 855 864 yield lineno, funcname, text, comments 856 865 directives.pop(idx) … … 870 879 871 880 for directive in directives: 872 881 if isinstance(directive, I18NDirectiveExtract): 873 messages = directive.extract(substream, ctxt) 882 messages = directive.extract( 883 substream, ctxt, error_callback=error_callback 884 ) 874 885 for funcname, text, comments in messages: 875 886 yield pos[1], funcname, text, comments 876 887 else: 877 888 messages = self.extract( 878 889 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) 880 892 for lineno, funcname, text, comments in messages: 881 893 yield lineno, funcname, text, comments 882 894 if comment: … … 885 897 def setup(self, template): 886 898 """Convenience function to register the `Translator` filter and the 887 899 related directives with the given template. 888 900 889 901 :param template: a `Template` instance 890 902 """ 891 903 template.filters.insert(0, self) … … 895 907 896 908 class MessageBuffer(object): 897 909 """Helper class for managing internationalized mixed content. 898 910 899 911 :since: version 0.5 900 912 """ 901 913 902 def __init__(self, directive=None ):914 def __init__(self, directive=None, error_callback=None): 903 915 """Initialize the message buffer. 904 916 905 917 :param params: comma-separated list of parameter names 906 918 :type params: `basestring` 907 919 :param lineno: the line number on which the first stream event … … 917 929 self.depth = 1 918 930 self.order = 1 919 931 self.stack = [0] 932 self.error_callback = error_callback 933 self.valid = True 920 934 self.subdirectives = {} 921 935 922 936 def append(self, kind, data, pos): 923 937 """Append a stream event to the buffer. 924 938 925 939 :param kind: the stream event kind 926 940 :param data: the event data 927 941 :param pos: the position of the event in the source 928 942 """ 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: 930 947 # The order needs to be +1 because a new START kind event will 931 948 # happen and we we need to wrap those events into our custom kind(s) 932 949 order = self.stack[-1] + 1 … … 948 965 if self.params: 949 966 param = self.params.pop(0) 950 967 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 962 986 self.string.append('%%(%s)s' % param) 963 987 self.events.setdefault(self.stack[-1], []).append((kind, data, pos)) 964 988 self.values[param] = (kind, data, pos) 965 989 else: 966 if kind is START: 990 if kind is START: 967 991 self.string.append(u'[%d:' % self.order) 968 992 self.stack.append(self.order) 969 993 self.events.setdefault(self.stack[-1], … … 986 1010 def translate(self, string, regex=re.compile(r'%\((\w+)\)s')): 987 1011 """Interpolate the given message translation with the events in the 988 1012 buffer and return the translated stream. 989 1013 990 1014 :param string: the translated message string 991 1015 """ 992 1016 substream = None 993 1017 994 1018 def yield_parts(string): 995 1019 for idx, part in enumerate(regex.split(string)): 996 1020 if idx % 2: … … 1077 1101 def parse_msg(string, regex=re.compile(r'(?:\[(\d+)\:)|(?<!\\)\]')): 1078 1102 """Parse a translated message using Genshi mixed content message 1079 1103 formatting. 1080 1104 1081 1105 >>> parse_msg("See [1:Help].") 1082 1106 [(0, 'See '), (1, 'Help'), (0, '.')] 1083 1107 1084 1108 >>> parse_msg("See [1:our [2:Help] page] for details.") 1085 1109 [(0, 'See '), (1, 'our '), (2, 'Help'), (1, ' page'), (0, ' for details.')] 1086 1110 1087 1111 >>> parse_msg("[2:Details] finden Sie in [1:Hilfe].") 1088 1112 [(2, 'Details'), (0, ' finden Sie in '), (1, 'Hilfe'), (0, '.')] 1089 1113 1090 1114 >>> parse_msg("[1:] Bilder pro Seite anzeigen.") 1091 1115 [(1, ''), (0, ' Bilder pro Seite anzeigen.')] 1092 1116 1093 1117 :param string: the translated message string 1094 1118 :return: a list of ``(order, string)`` tuples 1095 1119 :rtype: `list` … … 1121 1145 1122 1146 def extract_from_code(code, gettext_functions): 1123 1147 """Extract strings from Python bytecode. 1124 1148 1125 1149 >>> from genshi.template.eval import Expression 1126 1150 1127 1151 >>> expr = Expression('_("Hello")') 1128 1152 >>> list(extract_from_code(expr, Translator.GETTEXT_FUNCTIONS)) 1129 1153 [('_', u'Hello')] 1130 1154 1131 1155 >>> expr = Expression('ngettext("You have %(num)s item", ' 1132 1156 ... '"You have %(num)s items", num)') 1133 1157 >>> list(extract_from_code(expr, Translator.GETTEXT_FUNCTIONS)) 1134 1158 [('ngettext', (u'You have %(num)s item', u'You have %(num)s items', None))] 1135 1159 1136 1160 :param code: the `Code` object 1137 1161 :type code: `genshi.template.eval.Code` 1138 1162 :param gettext_functions: a sequence of function names … … 1172 1196 1173 1197 def extract(fileobj, keywords, comment_tags, options): 1174 1198 """Babel extraction method for Genshi templates. 1175 1199 1176 1200 :param fileobj: the file-like object the messages should be extracted from 1177 1201 :param keywords: a list of keywords (i.e. function names) that should be 1178 1202 recognized as translation functions … … 1205 1229 tmpl = template_class(fileobj, filename=getattr(fileobj, 'name', None), 1206 1230 encoding=encoding) 1207 1231 1232 error_callback = options.get('error_callback') 1233 1208 1234 translator = Translator(None, ignore_tags, include_attrs, extract_text) 1209 1235 if hasattr(tmpl, 'add_directives'): 1210 1236 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): 1212 1239 yield message 1213 1240 1214 1241 -
genshi/filters/tests/i18n.py
diff --git a/genshi/filters/tests/i18n.py b/genshi/filters/tests/i18n.py
a b 1427 1427 <p>BarFoo</p> 1428 1428 """, tmpl.generate().render()) 1429 1429 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 1430 1462 class ExtractTestCase(unittest.TestCase): 1431 1463 1432 1464 def test_markup_template_extraction(self):
