/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: Jelmer Vernooij
  • Date: 2020-04-05 19:11:34 UTC
  • mto: (7490.7.16 work)
  • mto: This revision was merged to the branch mainline in revision 7501.
  • Revision ID: jelmer@jelmer.uk-20200405191134-0aebh8ikiwygxma5
Populate the .gitignore file.

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)