~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/export_pot.py

  • Committer: Martin Packman
  • Date: 2011-11-22 18:48:16 UTC
  • mto: This revision was merged to the branch mainline in revision 6303.
  • Revision ID: martin.packman@canonical.com-20111122184816-srfa6yrcctscy0pq
Add export_pot._ModuleContext class for more structured source location tracking, and share option writing logic

Show diffs side-by-side

added added

removed removed

Lines of Context:
33
33
    commands as _mod_commands,
34
34
    errors,
35
35
    help_topics,
 
36
    option,
36
37
    plugin,
37
38
    help,
38
39
    )
68
69
    return s
69
70
 
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
 
71
130
class _PotExporter(object):
72
131
    """Write message details to output stream in .pot file format"""
73
132
 
74
133
    def __init__(self, outf):
75
134
        self.outf = outf
76
135
        self._msgids = set()
 
136
        self._module_contexts = {}
77
137
 
78
138
    def poentry(self, path, lineno, s, comment=None):
79
139
        if s in self._msgids:
92
152
            "\n".format(
93
153
                path=path, lineno=lineno, comment=comment, msg=_normalize(s)))
94
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
 
95
159
    def poentry_per_paragraph(self, path, lineno, msgid, include=None):
96
160
        # TODO: How to split long help?
97
161
        paragraphs = msgid.split('\n\n')
101
165
            self.poentry(path, lineno, p)
102
166
            lineno += p.count('\n') + 2
103
167
 
104
 
 
105
 
_LAST_CACHE = _LAST_CACHED_SRC = None
106
 
 
107
 
def _offsets_of_literal(src):
108
 
    global _LAST_CACHE, _LAST_CACHED_SRC
109
 
    if src == _LAST_CACHED_SRC:
110
 
        return _LAST_CACHE.copy()
111
 
 
112
 
    import ast
113
 
    root = ast.parse(src)
114
 
    offsets = {}
115
 
    for node in ast.walk(root):
116
 
        if not isinstance(node, ast.Str):
117
 
            continue
118
 
        offsets[node.s] = node.lineno - node.s.count('\n')
119
 
 
120
 
    _LAST_CACHED_SRC = src
121
 
    _LAST_CACHE = offsets.copy()
122
 
    return offsets
 
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
    if getattr(opt, 'title', None):
 
184
        exporter.poentry_in_context(context, opt.title,
 
185
            "title of {name!r} {what}".format(name=opt.name, what=note))
 
186
    if getattr(opt, 'help', None):
 
187
        exporter.poentry_in_context(context, opt.help,
 
188
            "help of {name!r} {what}".format(name=opt.name, what=note))
 
189
 
123
190
 
124
191
def _standard_options(exporter):
125
 
    from bzrlib.option import Option
126
 
    src = inspect.findsource(Option)[0]
127
 
    src = ''.join(src)
128
 
    path = 'bzrlib/option.py'
129
 
    offsets = _offsets_of_literal(src)
130
 
 
131
 
    for name in sorted(Option.OPTIONS.keys()):
132
 
        opt = Option.OPTIONS[name]
133
 
        if getattr(opt, 'hidden', False):
134
 
            continue
135
 
        if getattr(opt, 'title', None):
136
 
            lineno = offsets.get(opt.title, 9999)
137
 
            if lineno == 9999:
138
 
                note(gettext("%r is not found in bzrlib/option.py") % opt.title)
139
 
            exporter.poentry(path, lineno, opt.title,
140
 
                     'title of %r option' % name)
141
 
        if getattr(opt, 'help', None):
142
 
            lineno = offsets.get(opt.help, 9999)
143
 
            if lineno == 9999:
144
 
                note(gettext("%r is not found in bzrlib/option.py") % opt.help)
145
 
            exporter.poentry(path, lineno, opt.help,
146
 
                     'help of %r option' % name)
147
 
 
148
 
def _command_options(exporter, path, cmd):
149
 
    src, default_lineno = inspect.findsource(cmd.__class__)
150
 
    offsets = _offsets_of_literal(''.join(src))
 
192
    OPTIONS = option.Option.OPTIONS
 
193
    context = exporter.get_context(option)
 
194
    for name in sorted(OPTIONS.keys()):
 
195
        opt = OPTIONS[name]
 
196
        _write_option(exporter, context.from_string(name), opt, "option")
 
197
 
 
198
 
 
199
def _command_options(exporter, context, cmd):
 
200
    note = "option of {0!r} command".format(cmd.name())
151
201
    for opt in cmd.takes_options:
152
 
        if isinstance(opt, str):
153
 
            continue
154
 
        if getattr(opt, 'hidden', False):
155
 
            continue
156
 
        name = opt.name
157
 
        if getattr(opt, 'title', None):
158
 
            lineno = offsets.get(opt.title, default_lineno)
159
 
            exporter.poentry(path, lineno, opt.title,
160
 
                     'title of %r option of %r command' % (name, cmd.name()))
161
 
        if getattr(opt, 'help', None):
162
 
            lineno = offsets.get(opt.help, default_lineno)
163
 
            exporter.poentry(path, lineno, opt.help,
164
 
                     'help of %r option of %r command' % (name, cmd.name()))
 
202
        # String values in Command option lists are for global options
 
203
        if not isinstance(opt, str):
 
204
            _write_option(exporter, context, opt, note)
165
205
 
166
206
 
167
207
def _write_command_help(exporter, cmd):
168
 
    path = inspect.getfile(cmd.__class__)
169
 
    if path.endswith('.pyc'):
170
 
        path = path[:-1]
171
 
    path = os.path.relpath(path)
172
 
    src, lineno = inspect.findsource(cmd.__class__)
173
 
    offsets = _offsets_of_literal(''.join(src))
174
 
    lineno = offsets[cmd.__doc__]
175
 
    doc = inspect.getdoc(cmd)
 
208
    context = exporter.get_context(cmd.__class__)
 
209
    rawdoc = cmd.__doc__
 
210
    dcontext = context.from_string(rawdoc)
 
211
    doc = inspect.cleandoc(rawdoc)
176
212
 
177
213
    def exclude_usage(p):
178
214
        # ':Usage:' has special meaning in help topics.
180
216
        if p.splitlines()[0] != ':Usage:':
181
217
            return True
182
218
 
183
 
    exporter.poentry_per_paragraph(path, lineno, doc, exclude_usage)
184
 
    _command_options(exporter, path, cmd)
 
219
    exporter.poentry_per_paragraph(dcontext.path, dcontext.lineno, doc,
 
220
        exclude_usage)
 
221
    _command_options(exporter, context, cmd)
185
222
 
186
223
 
187
224
def _command_helps(exporter, plugin_name=None):
226
263
 
227
264
def _error_messages(exporter):
228
265
    """Extract fmt string from bzrlib.errors."""
229
 
    path = errors.__file__
230
 
    if path.endswith('.pyc'):
231
 
        path = path[:-1]
232
 
    offsets = _offsets_of_literal(open(path).read())
233
 
 
 
266
    context = exporter.get_context(errors)
234
267
    base_klass = errors.BzrError
235
268
    for name in dir(errors):
236
269
        klass = getattr(errors, name)
245
278
        fmt = getattr(klass, "_fmt", None)
246
279
        if fmt:
247
280
            note(gettext("Exporting message from error: %s"), name)
248
 
            exporter.poentry('bzrlib/errors.py',
249
 
                     offsets.get(fmt, 9999), fmt)
 
281
            exporter.poentry_in_context(context, fmt)
 
282
 
250
283
 
251
284
def _help_topics(exporter):
252
285
    topic_registry = help_topics.topic_registry
265
298
            exporter.poentry('dummy/help_topics/'+key+'/summary.txt',
266
299
                     1, summary)
267
300
 
 
301
 
268
302
def export_pot(outf, plugin=None):
269
303
    exporter = _PotExporter(outf)
270
304
    if plugin is None: