/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: 2005-10-19 10:11:57 UTC
  • mfrom: (1185.16.78)
  • mto: This revision was merged to the branch mainline in revision 1470.
  • Revision ID: robertc@robertcollins.net-20051019101157-17438d311e746b4f
mergeĀ fromĀ upstream

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