/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/export_pot.py

  • Committer: Robert Collins
  • Date: 2010-05-05 00:05:29 UTC
  • mto: This revision was merged to the branch mainline in revision 5206.
  • Revision ID: robertc@robertcollins.net-20100505000529-ltmllyms5watqj5u
Make 'pydoc bzrlib.tests.build_tree_shape' useful.

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 breezy 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
 
from __future__ import absolute_import
30
 
 
31
 
import inspect
32
 
import os
33
 
 
34
 
import breezy
35
 
from . import (
36
 
    commands as _mod_commands,
37
 
    errors,
38
 
    help_topics,
39
 
    option,
40
 
    plugin as _mod_plugin,
41
 
    )
42
 
from .sixish import PY3
43
 
from .trace import (
44
 
    mutter,
45
 
    note,
46
 
    )
47
 
from .i18n import gettext
48
 
 
49
 
 
50
 
def _escape(s):
51
 
    s = (s.replace('\\', '\\\\')
52
 
         .replace('\n', '\\n')
53
 
         .replace('\r', '\\r')
54
 
         .replace('\t', '\\t')
55
 
         .replace('"', '\\"')
56
 
         )
57
 
    return s
58
 
 
59
 
 
60
 
def _normalize(s):
61
 
    # This converts the various Python string types into a format that
62
 
    # is appropriate for .po files, namely much closer to C style.
63
 
    lines = s.split('\n')
64
 
    if len(lines) == 1:
65
 
        s = '"' + _escape(s) + '"'
66
 
    else:
67
 
        if not lines[-1]:
68
 
            del lines[-1]
69
 
            lines[-1] = lines[-1] + '\n'
70
 
        lineterm = '\\n"\n"'
71
 
        s = '""\n"' + lineterm.join(map(_escape, lines)) + '"'
72
 
    return s
73
 
 
74
 
 
75
 
def _parse_source(source_text, filename='<unknown>'):
76
 
    """Get object to lineno mappings from given source_text"""
77
 
    import ast
78
 
    cls_to_lineno = {}
79
 
    str_to_lineno = {}
80
 
    for node in ast.walk(ast.parse(source_text, filename)):
81
 
        # TODO: worry about duplicates?
82
 
        if isinstance(node, ast.ClassDef):
83
 
            # TODO: worry about nesting?
84
 
            cls_to_lineno[node.name] = node.lineno
85
 
        elif isinstance(node, ast.Str):
86
 
            # Python AST gives location of string literal as the line the
87
 
            # string terminates on. It's more useful to have the line the
88
 
            # string begins on. Unfortunately, counting back newlines is
89
 
            # only an approximation as the AST is ignorant of escaping.
90
 
            str_to_lineno[node.s] = node.lineno - node.s.count('\n')
91
 
    return cls_to_lineno, str_to_lineno
92
 
 
93
 
 
94
 
class _ModuleContext(object):
95
 
    """Record of the location within a source tree"""
96
 
 
97
 
    def __init__(self, path, lineno=1, _source_info=None):
98
 
        self.path = path
99
 
        self.lineno = lineno
100
 
        if _source_info is not None:
101
 
            self._cls_to_lineno, self._str_to_lineno = _source_info
102
 
 
103
 
    @classmethod
104
 
    def from_module(cls, module):
105
 
        """Get new context from module object and parse source for linenos"""
106
 
        sourcepath = inspect.getsourcefile(module)
107
 
        # TODO: fix this to do the right thing rather than rely on cwd
108
 
        relpath = os.path.relpath(sourcepath)
109
 
        return cls(relpath,
110
 
                   _source_info=_parse_source("".join(inspect.findsource(module)[0]), module.__file__))
111
 
 
112
 
    def from_class(self, cls):
113
 
        """Get new context with same details but lineno of class in source"""
114
 
        try:
115
 
            lineno = self._cls_to_lineno[cls.__name__]
116
 
        except (AttributeError, KeyError):
117
 
            mutter("Definition of %r not found in %r", cls, self.path)
118
 
            return self
119
 
        return self.__class__(self.path, lineno,
120
 
                              (self._cls_to_lineno, self._str_to_lineno))
121
 
 
122
 
    def from_string(self, string):
123
 
        """Get new context with same details but lineno of string in source"""
124
 
        try:
125
 
            lineno = self._str_to_lineno[string]
126
 
        except (AttributeError, KeyError):
127
 
            mutter("String %r not found in %r", string[:20], self.path)
128
 
            return self
129
 
        return self.__class__(self.path, lineno,
130
 
                              (self._cls_to_lineno, self._str_to_lineno))
131
 
 
132
 
 
133
 
class _PotExporter(object):
134
 
    """Write message details to output stream in .pot file format"""
135
 
 
136
 
    def __init__(self, outf, include_duplicates=False):
137
 
        self.outf = outf
138
 
        if include_duplicates:
139
 
            self._msgids = None
140
 
        else:
141
 
            self._msgids = set()
142
 
        self._module_contexts = {}
143
 
 
144
 
    def poentry(self, path, lineno, s, comment=None):
145
 
        if self._msgids is not None:
146
 
            if s in self._msgids:
147
 
                return
148
 
            self._msgids.add(s)
149
 
        if comment is None:
150
 
            comment = ''
151
 
        else:
152
 
            comment = "# %s\n" % comment
153
 
        mutter("Exporting msg %r at line %d in %r", s[:20], lineno, path)
154
 
        line = (
155
 
            "#: {path}:{lineno}\n"
156
 
            "{comment}"
157
 
            "msgid {msg}\n"
158
 
            "msgstr \"\"\n"
159
 
            "\n".format(
160
 
                path=path, lineno=lineno, comment=comment, msg=_normalize(s)))
161
 
        if not PY3:
162
 
            line = line.decode('utf-8')
163
 
        self.outf.write(line)
164
 
 
165
 
    def poentry_in_context(self, context, string, comment=None):
166
 
        context = context.from_string(string)
167
 
        self.poentry(context.path, context.lineno, string, comment)
168
 
 
169
 
    def poentry_per_paragraph(self, path, lineno, msgid, include=None):
170
 
        # TODO: How to split long help?
171
 
        paragraphs = msgid.split('\n\n')
172
 
        if include is not None:
173
 
            paragraphs = filter(include, paragraphs)
174
 
        for p in paragraphs:
175
 
            self.poentry(path, lineno, p)
176
 
            lineno += p.count('\n') + 2
177
 
 
178
 
    def get_context(self, obj):
179
 
        module = inspect.getmodule(obj)
180
 
        try:
181
 
            context = self._module_contexts[module.__name__]
182
 
        except KeyError:
183
 
            context = _ModuleContext.from_module(module)
184
 
            self._module_contexts[module.__name__] = context
185
 
        if inspect.isclass(obj):
186
 
            context = context.from_class(obj)
187
 
        return context
188
 
 
189
 
 
190
 
def _write_option(exporter, context, opt, note):
191
 
    if getattr(opt, 'hidden', False):
192
 
        return
193
 
    optname = opt.name
194
 
    if getattr(opt, 'title', None):
195
 
        exporter.poentry_in_context(context, opt.title,
196
 
                                    "title of {name!r} {what}".format(name=optname, what=note))
197
 
    for name, _, _, helptxt in opt.iter_switches():
198
 
        if name != optname:
199
 
            if opt.is_hidden(name):
200
 
                continue
201
 
            name = "=".join([optname, name])
202
 
        if helptxt:
203
 
            exporter.poentry_in_context(context, helptxt,
204
 
                                        "help of {name!r} {what}".format(name=name, what=note))
205
 
 
206
 
 
207
 
def _standard_options(exporter):
208
 
    OPTIONS = option.Option.OPTIONS
209
 
    context = exporter.get_context(option)
210
 
    for name in sorted(OPTIONS):
211
 
        opt = OPTIONS[name]
212
 
        _write_option(exporter, context.from_string(name), opt, "option")
213
 
 
214
 
 
215
 
def _command_options(exporter, context, cmd):
216
 
    note = "option of {0!r} command".format(cmd.name())
217
 
    for opt in cmd.takes_options:
218
 
        # String values in Command option lists are for global options
219
 
        if not isinstance(opt, str):
220
 
            _write_option(exporter, context, opt, note)
221
 
 
222
 
 
223
 
def _write_command_help(exporter, cmd):
224
 
    context = exporter.get_context(cmd.__class__)
225
 
    rawdoc = cmd.__doc__
226
 
    dcontext = context.from_string(rawdoc)
227
 
    doc = inspect.cleandoc(rawdoc)
228
 
 
229
 
    def exclude_usage(p):
230
 
        # ':Usage:' has special meaning in help topics.
231
 
        # This is usage example of command and should not be translated.
232
 
        if p.splitlines()[0] != ':Usage:':
233
 
            return True
234
 
 
235
 
    exporter.poentry_per_paragraph(dcontext.path, dcontext.lineno, doc,
236
 
                                   exclude_usage)
237
 
    _command_options(exporter, context, cmd)
238
 
 
239
 
 
240
 
def _command_helps(exporter, plugin_name=None):
241
 
    """Extract docstrings from path.
242
 
 
243
 
    This respects the Bazaar cmdtable/table convention and will
244
 
    only extract docstrings from functions mentioned in these tables.
245
 
    """
246
 
 
247
 
    # builtin commands
248
 
    for cmd_name in _mod_commands.builtin_command_names():
249
 
        command = _mod_commands.get_cmd_object(cmd_name, False)
250
 
        if command.hidden:
251
 
            continue
252
 
        if plugin_name is not None:
253
 
            # only export builtins if we are not exporting plugin commands
254
 
            continue
255
 
        note(gettext("Exporting messages from builtin command: %s"), cmd_name)
256
 
        _write_command_help(exporter, command)
257
 
 
258
 
    plugins = _mod_plugin.plugins()
259
 
    if plugin_name is not None and plugin_name not in plugins:
260
 
        raise errors.BzrError(gettext('Plugin %s is not loaded' % plugin_name))
261
 
    core_plugins = set(
262
 
        name for name in plugins
263
 
        if plugins[name].path().startswith(breezy.__path__[0]))
264
 
    # plugins
265
 
    for cmd_name in _mod_commands.plugin_command_names():
266
 
        command = _mod_commands.get_cmd_object(cmd_name, False)
267
 
        if command.hidden:
268
 
            continue
269
 
        if plugin_name is not None and command.plugin_name() != plugin_name:
270
 
            # if we are exporting plugin commands, skip plugins we have not
271
 
            # specified.
272
 
            continue
273
 
        if plugin_name is None and command.plugin_name() not in core_plugins:
274
 
            # skip non-core plugins
275
 
            # TODO: Support extracting from third party plugins.
276
 
            continue
277
 
        note(gettext("Exporting messages from plugin command: {0} in {1}").format(
278
 
             cmd_name, command.plugin_name()))
279
 
        _write_command_help(exporter, command)
280
 
 
281
 
 
282
 
def _error_messages(exporter):
283
 
    """Extract fmt string from breezy.errors."""
284
 
    context = exporter.get_context(errors)
285
 
    base_klass = errors.BzrError
286
 
    for name in dir(errors):
287
 
        klass = getattr(errors, name)
288
 
        if not inspect.isclass(klass):
289
 
            continue
290
 
        if not issubclass(klass, base_klass):
291
 
            continue
292
 
        if klass is base_klass:
293
 
            continue
294
 
        if klass.internal_error:
295
 
            continue
296
 
        fmt = getattr(klass, "_fmt", None)
297
 
        if fmt:
298
 
            note(gettext("Exporting message from error: %s"), name)
299
 
            exporter.poentry_in_context(context, fmt)
300
 
 
301
 
 
302
 
def _help_topics(exporter):
303
 
    topic_registry = help_topics.topic_registry
304
 
    for key in topic_registry.keys():
305
 
        doc = topic_registry.get(key)
306
 
        if isinstance(doc, str):
307
 
            exporter.poentry_per_paragraph(
308
 
                'dummy/help_topics/' + key + '/detail.txt',
309
 
                1, doc)
310
 
        elif callable(doc):  # help topics from files
311
 
            exporter.poentry_per_paragraph(
312
 
                'en/help_topics/' + key + '.txt',
313
 
                1, doc(key))
314
 
        summary = topic_registry.get_summary(key)
315
 
        if summary is not None:
316
 
            exporter.poentry('dummy/help_topics/' + key + '/summary.txt',
317
 
                             1, summary)
318
 
 
319
 
 
320
 
def export_pot(outf, plugin=None, include_duplicates=False):
321
 
    exporter = _PotExporter(outf, include_duplicates)
322
 
    if plugin is None:
323
 
        _standard_options(exporter)
324
 
        _command_helps(exporter)
325
 
        _error_messages(exporter)
326
 
        _help_topics(exporter)
327
 
    else:
328
 
        _command_helps(exporter, plugin)