~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/export_pot.py

  • Committer: Jelmer Vernooij
  • Date: 2011-12-05 14:12:23 UTC
  • mto: This revision was merged to the branch mainline in revision 6348.
  • Revision ID: jelmer@samba.org-20111205141223-8qxae4h37satlzgq
Move more functionality to vf_search.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2011 Canonical Ltd
 
2
#
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
#
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
#
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
16
 
 
17
# The normalize function is taken from pygettext which is distributed
 
18
# with Python under the Python License, which is GPL compatible.
 
19
 
 
20
"""Extract docstrings from Bazaar commands.
 
21
 
 
22
This module only handles bzrlib objects that use strings not directly wrapped
 
23
by a gettext() call. To generate a complete translation template file, this
 
24
output needs to be combined with that of xgettext or a similar command for
 
25
extracting those strings, as is done in the bzr Makefile. Sorting the output
 
26
is also left to that stage of the process.
 
27
"""
 
28
 
 
29
import inspect
 
30
import os
 
31
 
 
32
from bzrlib import (
 
33
    commands as _mod_commands,
 
34
    errors,
 
35
    help_topics,
 
36
    option,
 
37
    plugin,
 
38
    help,
 
39
    )
 
40
from bzrlib.trace import (
 
41
    mutter,
 
42
    note,
 
43
    )
 
44
from bzrlib.i18n import gettext
 
45
 
 
46
 
 
47
def _escape(s):
 
48
    s = (s.replace('\\', '\\\\')
 
49
        .replace('\n', '\\n')
 
50
        .replace('\r', '\\r')
 
51
        .replace('\t', '\\t')
 
52
        .replace('"', '\\"')
 
53
        )
 
54
    return s
 
55
 
 
56
def _normalize(s):
 
57
    # This converts the various Python string types into a format that
 
58
    # is appropriate for .po files, namely much closer to C style.
 
59
    lines = s.split('\n')
 
60
    if len(lines) == 1:
 
61
        s = '"' + _escape(s) + '"'
 
62
    else:
 
63
        if not lines[-1]:
 
64
            del lines[-1]
 
65
            lines[-1] = lines[-1] + '\n'
 
66
        lines = map(_escape, lines)
 
67
        lineterm = '\\n"\n"'
 
68
        s = '""\n"' + lineterm.join(lines) + '"'
 
69
    return s
 
70
 
 
71
 
 
72
def _parse_source(source_text):
 
73
    """Get object to lineno mappings from given source_text"""
 
74
    import ast
 
75
    cls_to_lineno = {}
 
76
    str_to_lineno = {}
 
77
    for node in ast.walk(ast.parse(source_text)):
 
78
        # TODO: worry about duplicates?
 
79
        if isinstance(node, ast.ClassDef):
 
80
            # TODO: worry about nesting?
 
81
            cls_to_lineno[node.name] = node.lineno
 
82
        elif isinstance(node, ast.Str):
 
83
            # Python AST gives location of string literal as the line the
 
84
            # string terminates on. It's more useful to have the line the
 
85
            # string begins on. Unfortunately, counting back newlines is
 
86
            # only an approximation as the AST is ignorant of escaping.
 
87
            str_to_lineno[node.s] = node.lineno - node.s.count('\n')
 
88
    return cls_to_lineno, str_to_lineno
 
89
 
 
90
 
 
91
class _ModuleContext(object):
 
92
    """Record of the location within a source tree"""
 
93
 
 
94
    def __init__(self, path, lineno=1, _source_info=None):
 
95
        self.path = path
 
96
        self.lineno = lineno
 
97
        if _source_info is not None:
 
98
            self._cls_to_lineno, self._str_to_lineno = _source_info
 
99
 
 
100
    @classmethod
 
101
    def from_module(cls, module):
 
102
        """Get new context from module object and parse source for linenos"""
 
103
        sourcepath = inspect.getsourcefile(module)
 
104
        # TODO: fix this to do the right thing rather than rely on cwd
 
105
        relpath = os.path.relpath(sourcepath)
 
106
        return cls(relpath,
 
107
            _source_info=_parse_source("".join(inspect.findsource(module)[0])))
 
108
 
 
109
    def from_class(self, cls):
 
110
        """Get new context with same details but lineno of class in source"""
 
111
        try:
 
112
            lineno = self._cls_to_lineno[cls.__name__]
 
113
        except (AttributeError, KeyError):
 
114
            mutter("Definition of %r not found in %r", cls, self.path)
 
115
            return self
 
116
        return self.__class__(self.path, lineno,
 
117
            (self._cls_to_lineno, self._str_to_lineno))
 
118
 
 
119
    def from_string(self, string):
 
120
        """Get new context with same details but lineno of string in source"""
 
121
        try:
 
122
            lineno = self._str_to_lineno[string]
 
123
        except (AttributeError, KeyError):
 
124
            mutter("String %r not found in %r", string[:20], self.path)
 
125
            return self
 
126
        return self.__class__(self.path, lineno,
 
127
            (self._cls_to_lineno, self._str_to_lineno))
 
128
 
 
129
 
 
130
class _PotExporter(object):
 
131
    """Write message details to output stream in .pot file format"""
 
132
 
 
133
    def __init__(self, outf):
 
134
        self.outf = outf
 
135
        self._msgids = set()
 
136
        self._module_contexts = {}
 
137
 
 
138
    def poentry(self, path, lineno, s, comment=None):
 
139
        if s in self._msgids:
 
140
            return
 
141
        self._msgids.add(s)
 
142
        if comment is None:
 
143
            comment = ''
 
144
        else:
 
145
            comment = "# %s\n" % comment
 
146
        mutter("Exporting msg %r at line %d in %r", s[:20], lineno, path)
 
147
        self.outf.write(
 
148
            "#: {path}:{lineno}\n"
 
149
            "{comment}"
 
150
            "msgid {msg}\n"
 
151
            "msgstr \"\"\n"
 
152
            "\n".format(
 
153
                path=path, lineno=lineno, comment=comment, msg=_normalize(s)))
 
154
 
 
155
    def poentry_in_context(self, context, string, comment=None):
 
156
        context = context.from_string(string)
 
157
        self.poentry(context.path, context.lineno, string, comment)
 
158
 
 
159
    def poentry_per_paragraph(self, path, lineno, msgid, include=None):
 
160
        # TODO: How to split long help?
 
161
        paragraphs = msgid.split('\n\n')
 
162
        if include is not None:
 
163
            paragraphs = filter(include, paragraphs)
 
164
        for p in paragraphs:
 
165
            self.poentry(path, lineno, p)
 
166
            lineno += p.count('\n') + 2
 
167
 
 
168
    def get_context(self, obj):
 
169
        module = inspect.getmodule(obj)
 
170
        try:
 
171
            context = self._module_contexts[module.__name__]
 
172
        except KeyError:
 
173
            context = _ModuleContext.from_module(module)
 
174
            self._module_contexts[module.__name__] = context
 
175
        if inspect.isclass(obj):
 
176
            context = context.from_class(obj)
 
177
        return context
 
178
 
 
179
 
 
180
def _write_option(exporter, context, opt, note):
 
181
    if getattr(opt, 'hidden', False):
 
182
        return   
 
183
    optname = opt.name
 
184
    if getattr(opt, 'title', None):
 
185
        exporter.poentry_in_context(context, opt.title,
 
186
            "title of {name!r} {what}".format(name=optname, what=note))
 
187
    for name, _, _, helptxt in opt.iter_switches():
 
188
        if name != optname:
 
189
            if opt.is_hidden(name):
 
190
                continue
 
191
            name = "=".join([optname, name])
 
192
        if helptxt:
 
193
            exporter.poentry_in_context(context, helptxt,
 
194
                "help of {name!r} {what}".format(name=name, what=note))
 
195
 
 
196
 
 
197
def _standard_options(exporter):
 
198
    OPTIONS = option.Option.OPTIONS
 
199
    context = exporter.get_context(option)
 
200
    for name in sorted(OPTIONS.keys()):
 
201
        opt = OPTIONS[name]
 
202
        _write_option(exporter, context.from_string(name), opt, "option")
 
203
 
 
204
 
 
205
def _command_options(exporter, context, cmd):
 
206
    note = "option of {0!r} command".format(cmd.name())
 
207
    for opt in cmd.takes_options:
 
208
        # String values in Command option lists are for global options
 
209
        if not isinstance(opt, str):
 
210
            _write_option(exporter, context, opt, note)
 
211
 
 
212
 
 
213
def _write_command_help(exporter, cmd):
 
214
    context = exporter.get_context(cmd.__class__)
 
215
    rawdoc = cmd.__doc__
 
216
    dcontext = context.from_string(rawdoc)
 
217
    doc = inspect.cleandoc(rawdoc)
 
218
 
 
219
    def exclude_usage(p):
 
220
        # ':Usage:' has special meaning in help topics.
 
221
        # This is usage example of command and should not be translated.
 
222
        if p.splitlines()[0] != ':Usage:':
 
223
            return True
 
224
 
 
225
    exporter.poentry_per_paragraph(dcontext.path, dcontext.lineno, doc,
 
226
        exclude_usage)
 
227
    _command_options(exporter, context, cmd)
 
228
 
 
229
 
 
230
def _command_helps(exporter, plugin_name=None):
 
231
    """Extract docstrings from path.
 
232
 
 
233
    This respects the Bazaar cmdtable/table convention and will
 
234
    only extract docstrings from functions mentioned in these tables.
 
235
    """
 
236
    from glob import glob
 
237
 
 
238
    # builtin commands
 
239
    for cmd_name in _mod_commands.builtin_command_names():
 
240
        command = _mod_commands.get_cmd_object(cmd_name, False)
 
241
        if command.hidden:
 
242
            continue
 
243
        if plugin_name is not None:
 
244
            # only export builtins if we are not exporting plugin commands
 
245
            continue
 
246
        note(gettext("Exporting messages from builtin command: %s"), cmd_name)
 
247
        _write_command_help(exporter, command)
 
248
 
 
249
    plugin_path = plugin.get_core_plugin_path()
 
250
    core_plugins = glob(plugin_path + '/*/__init__.py')
 
251
    core_plugins = [os.path.basename(os.path.dirname(p))
 
252
                        for p in core_plugins]
 
253
    # plugins
 
254
    for cmd_name in _mod_commands.plugin_command_names():
 
255
        command = _mod_commands.get_cmd_object(cmd_name, False)
 
256
        if command.hidden:
 
257
            continue
 
258
        if plugin_name is not None and command.plugin_name() != plugin_name:
 
259
            # if we are exporting plugin commands, skip plugins we have not specified.
 
260
            continue
 
261
        if plugin_name is None and command.plugin_name() not in core_plugins:
 
262
            # skip non-core plugins
 
263
            # TODO: Support extracting from third party plugins.
 
264
            continue
 
265
        note(gettext("Exporting messages from plugin command: {0} in {1}").format(
 
266
             cmd_name, command.plugin_name() ))
 
267
        _write_command_help(exporter, command)
 
268
 
 
269
 
 
270
def _error_messages(exporter):
 
271
    """Extract fmt string from bzrlib.errors."""
 
272
    context = exporter.get_context(errors)
 
273
    base_klass = errors.BzrError
 
274
    for name in dir(errors):
 
275
        klass = getattr(errors, name)
 
276
        if not inspect.isclass(klass):
 
277
            continue
 
278
        if not issubclass(klass, base_klass):
 
279
            continue
 
280
        if klass is base_klass:
 
281
            continue
 
282
        if klass.internal_error:
 
283
            continue
 
284
        fmt = getattr(klass, "_fmt", None)
 
285
        if fmt:
 
286
            note(gettext("Exporting message from error: %s"), name)
 
287
            exporter.poentry_in_context(context, fmt)
 
288
 
 
289
 
 
290
def _help_topics(exporter):
 
291
    topic_registry = help_topics.topic_registry
 
292
    for key in topic_registry.keys():
 
293
        doc = topic_registry.get(key)
 
294
        if isinstance(doc, str):
 
295
            exporter.poentry_per_paragraph(
 
296
                    'dummy/help_topics/'+key+'/detail.txt',
 
297
                    1, doc)
 
298
        elif callable(doc): # help topics from files
 
299
            exporter.poentry_per_paragraph(
 
300
                    'en/help_topics/'+key+'.txt',
 
301
                    1, doc(key))
 
302
        summary = topic_registry.get_summary(key)
 
303
        if summary is not None:
 
304
            exporter.poentry('dummy/help_topics/'+key+'/summary.txt',
 
305
                     1, summary)
 
306
 
 
307
 
 
308
def export_pot(outf, plugin=None):
 
309
    exporter = _PotExporter(outf)
 
310
    if plugin is None:
 
311
        _standard_options(exporter)
 
312
        _command_helps(exporter)
 
313
        _error_messages(exporter)
 
314
        _help_topics(exporter)
 
315
    else:
 
316
        _command_helps(exporter, plugin)