20
20
"""Extract docstrings from Bazaar commands.
22
This module only handles breezy objects that use strings not directly wrapped
22
This module only handles brzlib objects that use strings not directly wrapped
23
23
by a gettext() call. To generate a complete translation template file, this
24
24
output needs to be combined with that of xgettext or a similar command for
25
25
extracting those strings, as is done in the bzr Makefile. Sorting the output
26
26
is also left to that stage of the process.
29
from __future__ import absolute_import
35
35
commands as _mod_commands,
39
plugin as _mod_plugin,
42
from brzlib.trace import (
45
from .i18n import gettext
46
from brzlib.i18n import gettext
49
50
s = (s.replace('\\', '\\\\')
59
59
# This converts the various Python string types into a format that
60
60
# is appropriate for .po files, namely much closer to C style.
67
67
lines[-1] = lines[-1] + '\n'
68
lines = map(_escape, lines)
68
69
lineterm = '\\n"\n"'
69
s = '""\n"' + lineterm.join(map(_escape, lines)) + '"'
70
s = '""\n"' + lineterm.join(lines) + '"'
73
def _parse_source(source_text, filename='<unknown>'):
74
def _parse_source(source_text):
74
75
"""Get object to lineno mappings from given source_text"""
78
for node in ast.walk(ast.parse(source_text, filename)):
79
for node in ast.walk(ast.parse(source_text)):
79
80
# TODO: worry about duplicates?
80
81
if isinstance(node, ast.ClassDef):
81
82
# TODO: worry about nesting?
85
86
# string terminates on. It's more useful to have the line the
86
87
# string begins on. Unfortunately, counting back newlines is
87
88
# only an approximation as the AST is ignorant of escaping.
88
str_to_lineno[node.s] = node.lineno - (0 if sys.version_info >= (3, 8) else node.s.count('\n'))
89
str_to_lineno[node.s] = node.lineno - node.s.count('\n')
89
90
return cls_to_lineno, str_to_lineno
105
106
# TODO: fix this to do the right thing rather than rely on cwd
106
107
relpath = os.path.relpath(sourcepath)
107
108
return cls(relpath,
108
_source_info=_parse_source("".join(inspect.findsource(module)[0]), module.__file__))
109
_source_info=_parse_source("".join(inspect.findsource(module)[0])))
110
111
def from_class(self, cls):
111
112
"""Get new context with same details but lineno of class in source"""
115
116
mutter("Definition of %r not found in %r", cls, self.path)
117
118
return self.__class__(self.path, lineno,
118
(self._cls_to_lineno, self._str_to_lineno))
119
(self._cls_to_lineno, self._str_to_lineno))
120
121
def from_string(self, string):
121
122
"""Get new context with same details but lineno of string in source"""
125
126
mutter("String %r not found in %r", string[:20], self.path)
127
128
return self.__class__(self.path, lineno,
128
(self._cls_to_lineno, self._str_to_lineno))
129
(self._cls_to_lineno, self._str_to_lineno))
131
132
class _PotExporter(object):
150
151
comment = "# %s\n" % comment
151
152
mutter("Exporting msg %r at line %d in %r", s[:20], lineno, path)
153
154
"#: {path}:{lineno}\n"
158
159
path=path, lineno=lineno, comment=comment, msg=_normalize(s)))
159
self.outf.write(line)
161
161
def poentry_in_context(self, context, string, comment=None):
162
162
context = context.from_string(string)
186
186
def _write_option(exporter, context, opt, note):
187
187
if getattr(opt, 'hidden', False):
189
189
optname = opt.name
190
190
if getattr(opt, 'title', None):
191
191
exporter.poentry_in_context(context, opt.title,
192
"title of {name!r} {what}".format(name=optname, what=note))
192
"title of {name!r} {what}".format(name=optname, what=note))
193
193
for name, _, _, helptxt in opt.iter_switches():
194
194
if name != optname:
195
195
if opt.is_hidden(name):
197
197
name = "=".join([optname, name])
199
199
exporter.poentry_in_context(context, helptxt,
200
"help of {name!r} {what}".format(name=name, what=note))
200
"help of {name!r} {what}".format(name=name, what=note))
203
203
def _standard_options(exporter):
204
204
OPTIONS = option.Option.OPTIONS
205
205
context = exporter.get_context(option)
206
for name in sorted(OPTIONS):
206
for name in sorted(OPTIONS.keys()):
207
207
opt = OPTIONS[name]
208
208
_write_option(exporter, context.from_string(name), opt, "option")
231
231
exporter.poentry_per_paragraph(dcontext.path, dcontext.lineno, doc,
233
233
_command_options(exporter, context, cmd)
251
252
note(gettext("Exporting messages from builtin command: %s"), cmd_name)
252
253
_write_command_help(exporter, command)
254
plugins = _mod_plugin.plugins()
255
if plugin_name is not None and plugin_name not in plugins:
256
raise errors.BzrError(gettext('Plugin %s is not loaded' % plugin_name))
258
name for name in plugins
259
if plugins[name].path().startswith(breezy.__path__[0]))
255
plugin_path = plugin.get_core_plugin_path()
256
core_plugins = glob(plugin_path + '/*/__init__.py')
257
core_plugins = [os.path.basename(os.path.dirname(p))
258
for p in core_plugins]
261
260
for cmd_name in _mod_commands.plugin_command_names():
262
261
command = _mod_commands.get_cmd_object(cmd_name, False)
263
262
if command.hidden:
265
264
if plugin_name is not None and command.plugin_name() != plugin_name:
266
# if we are exporting plugin commands, skip plugins we have not
265
# if we are exporting plugin commands, skip plugins we have not specified.
269
267
if plugin_name is None and command.plugin_name() not in core_plugins:
270
268
# skip non-core plugins
271
269
# TODO: Support extracting from third party plugins.
273
271
note(gettext("Exporting messages from plugin command: {0} in {1}").format(
274
cmd_name, command.plugin_name()))
272
cmd_name, command.plugin_name() ))
275
273
_write_command_help(exporter, command)
278
276
def _error_messages(exporter):
279
"""Extract fmt string from breezy.errors."""
277
"""Extract fmt string from brzlib.errors."""
280
278
context = exporter.get_context(errors)
281
279
base_klass = errors.BzrError
282
280
for name in dir(errors):
301
299
doc = topic_registry.get(key)
302
300
if isinstance(doc, str):
303
301
exporter.poentry_per_paragraph(
304
'dummy/help_topics/' + key + '/detail.txt',
306
elif callable(doc): # help topics from files
302
'dummy/help_topics/'+key+'/detail.txt',
304
elif callable(doc): # help topics from files
307
305
exporter.poentry_per_paragraph(
308
'en/help_topics/' + key + '.txt',
306
'en/help_topics/'+key+'.txt',
310
308
summary = topic_registry.get_summary(key)
311
309
if summary is not None:
312
exporter.poentry('dummy/help_topics/' + key + '/summary.txt',
310
exporter.poentry('dummy/help_topics/'+key+'/summary.txt',
316
314
def export_pot(outf, plugin=None, include_duplicates=False):