# -*- coding: utf-8 -*- # # Copyright (C) 2011-2021 Edgewall Software # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. The terms # are also available at https://trac.edgewall.org/wiki/TracLicense. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at https://trac.edgewall.org/log/. """Extra commands for setup.py. We provide a few extra command classes in `l10n_cmdclass` for localization tasks. We also modify the standard commands `distutils.command.build` and `setuptools.command.install_lib` classes in order to call the l10n commands for compiling catalogs at the right time during install. """ from html.parser import HTMLParser import io import os import re from tokenize import generate_tokens, COMMENT, NAME, OP, STRING from jinja2.ext import babel_extract as jinja2_extractor from distutils import log as distlog from distutils.cmd import Command from distutils.command.build import build as _build from distutils.errors import DistutilsOptionError from setuptools.command.install_lib import install_lib as _install_lib def simplify_message(message): """Transforms an extracted messsage (string or tuple) into one in which the repeated white-space has been simplified to a single space. """ tuple_len = len(message) if isinstance(message, tuple) else 0 if tuple_len: message = message[0] message = ' '.join(message.split()) if tuple_len: message = (message,) + (None,) * (tuple_len - 1) return message class ScriptExtractor(HTMLParser): def __init__(self, out): HTMLParser.__init__(self) self.out = out self.in_javascript = False def handle_starttag(self, tag, attrs): if tag == 'script': self.in_javascript = True def handle_startendtag(self, tag, attrs): self.in_javascript = False def handle_charref(self, name): if self.in_javascript: self.out.write('&#%s;' % name) def handle_entityref(self, name): if self.in_javascript: self.out.write('&%s;' % name) def handle_data(self, data): if self.in_javascript: self.out.write(data) def handle_endtag(self, tag): self.in_javascript = False def no_op(*args, **kwargs): pass handle_comment = handle_decl = handle_pi = no_op try: from babel.messages.catalog import TranslationError from babel.messages.extract import extract_javascript from babel.messages.frontend import extract_messages, init_catalog, \ compile_catalog, update_catalog from babel.messages.pofile import read_po from babel.support import Translations from babel.util import parse_encoding _DEFAULT_KWARGS_MAPS = { 'Option': {'doc': 4}, 'BoolOption': {'doc': 4}, 'IntOption': {'doc': 4}, 'FloatOption': {'doc': 4}, 'ListOption': {'doc': 6}, 'ChoiceOption': {'doc': 4}, 'PathOption': {'doc': 4}, 'ExtensionOption': {'doc': 5}, 'OrderedExtensionsOption': {'doc': 6}, } _DEFAULT_CLEANDOC_KEYWORDS = ( 'ConfigSection', 'Option', 'BoolOption', 'IntOption', 'FloatOption', 'ListOption', 'ChoiceOption', 'PathOption', 'ExtensionOption', 'OrderedExtensionsOption', 'cleandoc_', ) def extract_python(fileobj, keywords, comment_tags, options): """Extract messages from Python source code, This is patched extract_python from Babel to support keyword argument mapping. `kwargs_maps` option: names of keyword arguments will be mapping to index of messages array. `cleandoc_keywords` option: a list of keywords to clean up the extracted messages with `cleandoc`. """ from trac.util.text import cleandoc funcname = lineno = message_lineno = None kwargs_maps = func_kwargs_map = None call_stack = -1 buf = [] messages = [] messages_kwargs = {} translator_comments = [] in_def = in_translator_comments = False comment_tag = None encoding = str(parse_encoding(fileobj) or options.get('encoding', 'iso-8859-1')) kwargs_maps = _DEFAULT_KWARGS_MAPS.copy() if 'kwargs_maps' in options: kwargs_maps.update(options['kwargs_maps']) cleandoc_keywords = set(_DEFAULT_CLEANDOC_KEYWORDS) if 'cleandoc_keywords' in options: cleandoc_keywords.update(options['cleandoc_keywords']) tokens = generate_tokens(fileobj.readline) tok = value = None for _ in tokens: prev_tok, prev_value = tok, value tok, value, (lineno, _), _, _ = _ if call_stack == -1 and tok == NAME and value in ('def', 'class'): in_def = True elif tok == OP and value == '(': if in_def: # Avoid false positives for declarations such as: # def gettext(arg='message'): in_def = False continue if funcname: message_lineno = lineno call_stack += 1 kwarg_name = None elif in_def and tok == OP and value == ':': # End of a class definition without parens in_def = False continue elif call_stack == -1 and tok == COMMENT: # Strip the comment token from the line value = value.decode(encoding)[1:].strip() if in_translator_comments and \ translator_comments[-1][0] == lineno - 1: # We're already inside a translator comment, continue # appending translator_comments.append((lineno, value)) continue # If execution reaches this point, let's see if comment line # starts with one of the comment tags for comment_tag in comment_tags: if value.startswith(comment_tag): in_translator_comments = True translator_comments.append((lineno, value)) break elif funcname and call_stack == 0: if tok == OP and value == ')': if buf: message = ''.join(buf) if kwarg_name in func_kwargs_map: messages_kwargs[kwarg_name] = message else: messages.append(message) del buf[:] else: messages.append(None) for name, message in messages_kwargs.items(): if name not in func_kwargs_map: continue index = func_kwargs_map[name] while index >= len(messages): messages.append(None) messages[index - 1] = message if funcname in cleandoc_keywords: messages = [m and cleandoc(m) for m in messages] if len(messages) > 1: messages = tuple(messages) else: messages = messages[0] # Comments don't apply unless they immediately preceed the # message if translator_comments and \ translator_comments[-1][0] < message_lineno - 1: translator_comments = [] yield (message_lineno, funcname, messages, [comment[1] for comment in translator_comments]) funcname = lineno = message_lineno = None kwarg_name = func_kwargs_map = None call_stack = -1 messages = [] messages_kwargs = {} translator_comments = [] in_translator_comments = False elif tok == STRING: # Unwrap quotes in a safe manner, maintaining the string's # encoding # https://sourceforge.net/tracker/?func=detail&atid=355470& # aid=617979&group_id=5470 value = eval('# coding=%s\n%s' % (encoding, value), {'__builtins__':{}}, {}) if isinstance(value, bytes): value = value.decode(encoding) buf.append(value) elif tok == OP and value == '=' and prev_tok == NAME: kwarg_name = prev_value elif tok == OP and value == ',': if buf: message = ''.join(buf) if kwarg_name in func_kwargs_map: messages_kwargs[kwarg_name] = message else: messages.append(message) del buf[:] else: messages.append(None) kwarg_name = None if translator_comments: # We have translator comments, and since we're on a # comma(,) user is allowed to break into a new line # Let's increase the last comment's lineno in order # for the comment to still be a valid one old_lineno, old_comment = translator_comments.pop() translator_comments.append((old_lineno+1, old_comment)) elif call_stack > 0 and tok == OP and value == ')': call_stack -= 1 elif funcname and call_stack == -1: funcname = func_kwargs_map = kwarg_name = None elif tok == NAME and value in keywords: funcname = value func_kwargs_map = kwargs_maps.get(funcname, {}) kwarg_name = None def extract_javascript_script(fileobj, keywords, comment_tags, options): """Extract messages from Javascript embedded in