~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/doc_generate/writers/texinfo.py

Add bzrlib.pyutils, which has get_named_object, a wrapper around __import__.

This is used to replace various ad hoc implementations of the same logic,
notably the version used in registry's _LazyObjectGetter which had a bug when
getting a module without also getting a member.  And of course, this new
function has unit tests, unlike the replaced code.

This also adds a KnownHooksRegistry subclass to provide a more natural home for
some other logic.

I'm not thrilled about the name of the new module or the new functions, but it's
hard to think of good names for such generic functionality.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2010 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
"""A sphinx/docutil writer producing texinfo output."""
 
18
 
 
19
from docutils import (
 
20
    nodes,
 
21
    writers,
 
22
    )
 
23
 
 
24
DEBUG = 0
 
25
 
 
26
class TexinfoWriter(writers.Writer):
 
27
 
 
28
    supported = ('texinfo',)
 
29
    settings_spec = ('No options here.', '', ())
 
30
    settings_defaults = {}
 
31
 
 
32
    output = None
 
33
 
 
34
    def __init__(self, builder):
 
35
        writers.Writer.__init__(self)
 
36
        self.builder = builder
 
37
 
 
38
    def translate(self):
 
39
        visitor = TexinfoTranslator(self.document, self.builder)
 
40
        self.document.walkabout(visitor)
 
41
        self.output = visitor.body
 
42
 
 
43
 
 
44
class TexinfoTranslator(nodes.NodeVisitor):
 
45
 
 
46
    section_names = ['chapter', 'section', 'subsection', 'subsubsection']
 
47
    """texinfo section names differ from the sphinx ones.
 
48
 
 
49
    Since this can be confusing, the correspondences are shown below
 
50
    (shpinx -> texinfo):
 
51
    part       -> chapter
 
52
    chapter    -> section
 
53
    section    -> subsection
 
54
    subsection -> subsubsection
 
55
 
 
56
    Additionally, sphinx defines subsubsections and paragraphs which are
 
57
    handled as @heading (unnumbered).
 
58
    """
 
59
 
 
60
    def __init__(self, document, builder):
 
61
        nodes.NodeVisitor.__init__(self, document)
 
62
        self.builder = builder
 
63
        # toctree uses some nodes for different purposes (namely:
 
64
        # compact_paragraph, bullet_list) that needs to know when they are
 
65
        # processing a toctree.
 
66
        self.in_toctree = False
 
67
        # sections can be embedded and produce different directives depending
 
68
        # on the depth.
 
69
        self.section_level = -1
 
70
        # By default paragraghs are separated by newlines, but there are some
 
71
        # exceptions that set it to '' for some subtrees instead
 
72
        self.paragraph_sep = '\n'
 
73
 
 
74
    # The whole document
 
75
 
 
76
    def visit_document(self, node):
 
77
        if DEBUG:
 
78
            import sys
 
79
            sys.stdout.write(node.pformat().encode('utf8'))
 
80
        set_item_list_collector(node, 'text')
 
81
 
 
82
    def depart_document(self, node):
 
83
        # FIXME: info requires a Top node for each info file, but unless we
 
84
        # chose a global layout to divide the overall documentation into a set
 
85
        # of info files, there is no criteria to decide for a title.
 
86
        top_cmd = '''\
 
87
This file has been converted using a beta rst->texinfo converter. 
 
88
Most of the info links are currently bogus, don't report bugs about them,
 
89
this is currently worked on.
 
90
@node Top
 
91
@top Placeholder
 
92
'''
 
93
        self.body = top_cmd + ''.join(node['text'])
 
94
 
 
95
    # Layout
 
96
 
 
97
    def visit_section(self, node):
 
98
        self.section_level += 1
 
99
        set_item_list_collector(node, 'text')
 
100
 
 
101
    def depart_section(self, node):
 
102
        title = node['title']
 
103
        ids = node.get('ids', [])
 
104
        try:
 
105
            section_name = self.section_names[self.section_level]
 
106
        except IndexError:
 
107
            # Just use @heading, it's not numbered anyway
 
108
            section_name = 'heading'
 
109
        if ids:
 
110
            # There shouldn't be different ids for a section, so until we
 
111
            # encounter bugs, just take the first one.
 
112
            node_cmd = '@node %s\n' % (ids[0],)
 
113
        else:
 
114
            node_cmd = ''
 
115
        section_cmd = '@%s %s\n' % (section_name, title)
 
116
        text = ''.join(node['text'])
 
117
        node.parent.collect_text(node_cmd + section_cmd + text)
 
118
        self.section_level -= 1
 
119
 
 
120
    def visit_topic(self, node):
 
121
        pass
 
122
 
 
123
    def depart_topic(self, node):
 
124
        pass
 
125
 
 
126
    def visit_paragraph(self, node):
 
127
        set_item_list_collector(node, 'text')
 
128
 
 
129
    def depart_paragraph(self, node):
 
130
        # End the paragraph with a new line (or '' depending on the parent) and
 
131
        # leave a blank line after it.
 
132
        text = ''.join(node['text']) + self.paragraph_sep * 2
 
133
        node.parent.collect_text(text)
 
134
 
 
135
    def visit_compact_paragraph(self, node):
 
136
        set_item_list_collector(node, 'text')
 
137
        if node.has_key('toctree'):
 
138
            self.in_toctree = True
 
139
        elif self.in_toctree:
 
140
            set_item_collector(node, 'reference')
 
141
 
 
142
    def depart_compact_paragraph(self, node):
 
143
        # FIXME: Using a different visitor specific to toctree may be a better
 
144
        # design and makes code clearer. -- vila 20100708
 
145
        if node.has_key('toctree'):
 
146
            node.parent.collect_text('@menu\n')
 
147
            node.parent.collect_text(''.join(node['text']))
 
148
            node.parent.collect_text('@end menu\n')
 
149
            self.in_toctree = False
 
150
        elif self.in_toctree:
 
151
            # * FIRST-ENTRY-NAME:(FILENAME)NODENAME.     DESCRIPTION
 
152
            # XXX: the file name should probably be adjusted to the targeted
 
153
            # info file name
 
154
            node_name, file_name, entry_name = node['reference']
 
155
            if not node_name:
 
156
                node_name = entry_name
 
157
            description = '' # We can't specify a description in rest AFAICS
 
158
            # XXX: What if :maxdepth: is not 1 ?
 
159
            text = '* %s: (%s)%s. %s\n' % (entry_name, file_name,
 
160
                                           node_name, description)
 
161
            node.parent.collect_text(text)
 
162
        else:
 
163
            # End the paragraph with a new line (or '' depending on the parent)
 
164
            # and leave a blank line after it.
 
165
            text = ''.join(node['text']) + self.paragraph_sep * 2
 
166
            node.parent.collect_text(text)
 
167
 
 
168
    def visit_literal_block(self, node):
 
169
        set_item_collector(node, 'text')
 
170
 
 
171
    def depart_literal_block(self, node):
 
172
        text = '@samp{%s}' % ''.join(node['text']) + self.paragraph_sep * 2
 
173
        node.parent.collect_text(text)
 
174
 
 
175
    def visit_block_quote(self, node):
 
176
        set_item_list_collector(node, 'text')
 
177
 
 
178
    def depart_block_quote(self, node):
 
179
        node.parent.collect_text('@example\n')
 
180
        node.parent.collect_text(''.join(node['text']))
 
181
        node.parent.collect_text('@end example\n\n')
 
182
 
 
183
    def depart_warning(self, node):
 
184
        pass
 
185
 
 
186
    def visit_warning(self, node):
 
187
        raise nodes.SkipNode # Not implemented yet
 
188
 
 
189
    def visit_note(self, node):
 
190
        raise nodes.SkipNode # Not implemented yet
 
191
 
 
192
    def depart_note(self, node):
 
193
        pass
 
194
 
 
195
    def visit_footnote(self, node):
 
196
        raise nodes.SkipNode # Not implemented yet
 
197
 
 
198
    def depart_footnote(self, node):
 
199
        pass
 
200
 
 
201
    def visit_comment(self, node):
 
202
        raise nodes.SkipNode # Not implemented yet
 
203
 
 
204
    # Attributes
 
205
 
 
206
    def visit_title(self, node):
 
207
        set_item_collector(node, 'text')
 
208
 
 
209
    def depart_title(self, node):
 
210
        node.parent['title'] = node['text']
 
211
 
 
212
    def visit_label(self, node):
 
213
        raise nodes.SkipNode # Not implemented yet
 
214
 
 
215
    def visit_substitution_definition(self, node):
 
216
        raise nodes.SkipNode # Not implemented yet
 
217
 
 
218
    # Plain text
 
219
 
 
220
    def visit_Text(self, node):
 
221
        pass
 
222
 
 
223
    def depart_Text(self, node):
 
224
        text = node.astext()
 
225
        if '@' in text:
 
226
            text = text.replace('@', '@@')
 
227
        if '{' in text:
 
228
            text = text.replace('{', '@{')
 
229
        if '}' in text:
 
230
            text = text.replace('}', '@}')
 
231
        node.parent.collect_text(text)
 
232
 
 
233
 
 
234
    # Styled text
 
235
 
 
236
    def visit_emphasis(self, node):
 
237
        set_item_collector(node, 'text')
 
238
 
 
239
    def depart_emphasis(self, node):
 
240
        text = '@emph{%s}' % node['text']
 
241
        node.parent.collect_text(text)
 
242
 
 
243
    def visit_strong(self, node):
 
244
        set_item_collector(node, 'text')
 
245
 
 
246
    def depart_strong(self, node):
 
247
        text = '@strong{%s}' % node['text']
 
248
        node.parent.collect_text(text)
 
249
 
 
250
    def visit_literal(self, node):
 
251
        set_item_collector(node, 'text')
 
252
 
 
253
    def depart_literal(self, node):
 
254
        text = '@code{%s}' % node['text']
 
255
        node.parent.collect_text(text)
 
256
 
 
257
    # Lists
 
258
 
 
259
    def _decorate_list(self, item_list, collect, item_fmt='%s',
 
260
                       head=None, foot=None):
 
261
        if head is not None:
 
262
            collect(head)
 
263
        for item in item_list:
 
264
            collect(item_fmt % item)
 
265
        if foot is not None:
 
266
            collect(foot)
 
267
 
 
268
    def visit_bullet_list(self, node):
 
269
        set_item_list_collector(node, 'list_item')
 
270
 
 
271
    def depart_bullet_list(self, node):
 
272
        l = node['list_item']
 
273
        if self.in_toctree:
 
274
            self._decorate_list(node['list_item'], node.parent.collect_text)
 
275
        else:
 
276
            self._decorate_list(node['list_item'], node.parent.collect_text,
 
277
                                '@item\n%s',
 
278
                                # FIXME: Should respect the 'bullet' attribute
 
279
                                '@itemize @bullet\n', '@end itemize\n')
 
280
 
 
281
    def visit_enumerated_list(self, node):
 
282
        set_item_list_collector(node, 'list_item')
 
283
 
 
284
    def depart_enumerated_list(self, node):
 
285
        self._decorate_list(node['list_item'], node.parent.collect_text,
 
286
                            '@item\n%s',
 
287
                            '@enumerate\n', '@end enumerate\n')
 
288
 
 
289
    def visit_definition_list(self, node):
 
290
        raise nodes.SkipNode # Not implemented yet
 
291
 
 
292
    def depart_definition_list(self, node):
 
293
        raise nodes.SkipNode # Not implemented yet
 
294
 
 
295
    def visit_definition_list_item(self, node):
 
296
        raise nodes.SkipNode # Not implemented yet
 
297
 
 
298
    def depart_definition_list_item(self, node):
 
299
        pass
 
300
 
 
301
    def visit_term(self, node):
 
302
        raise nodes.SkipNode # Not implemented yet
 
303
 
 
304
    def depart_term(self, node):
 
305
        pass
 
306
 
 
307
    def visit_definition(self, node):
 
308
        raise nodes.SkipNode # Not implemented yet
 
309
 
 
310
    def depart_definition(self, node):
 
311
        pass
 
312
 
 
313
    def visit_field_list(self, node):
 
314
        raise nodes.SkipNode # Not implemented yet
 
315
 
 
316
    def depart_field_list(self, node):
 
317
        pass
 
318
 
 
319
    def visit_field(self, node):
 
320
        raise nodes.SkipNode # Not implemented yet
 
321
 
 
322
    def depart_field(self, node):
 
323
        pass
 
324
 
 
325
    def visit_field_name(self, node):
 
326
        raise nodes.SkipNode # Not implemented yet
 
327
 
 
328
    def depart_field_name(self, node):
 
329
        pass
 
330
 
 
331
    def visit_field_body(self, node):
 
332
        raise nodes.SkipNode # Not implemented yet
 
333
 
 
334
    def depart_field_body(self, node):
 
335
        pass
 
336
 
 
337
    def visit_list_item(self, node):
 
338
        set_item_list_collector(node, 'text')
 
339
 
 
340
    def depart_list_item(self, node):
 
341
        text = ''.join(node['text'])
 
342
        node.parent.collect_list_item(text)
 
343
 
 
344
    def visit_option_list(self, node):
 
345
        raise nodes.SkipNode # Not implemented yet
 
346
 
 
347
    def depart_option_list(self, node):
 
348
        pass
 
349
 
 
350
    def visit_option_list_item(self, node):
 
351
        pass
 
352
 
 
353
    def depart_option_list_item(self, node):
 
354
        pass
 
355
 
 
356
    def visit_option_group(self, node):
 
357
        pass
 
358
 
 
359
    def depart_option_group(self, node):
 
360
        pass
 
361
 
 
362
    def visit_option(self, node):
 
363
        pass
 
364
 
 
365
    def depart_option(self, node):
 
366
        pass
 
367
 
 
368
    def visit_option_string(self, node):
 
369
        pass
 
370
    def depart_option_string(self, node):
 
371
        pass
 
372
 
 
373
    def visit_option_argument(self, node):
 
374
        pass
 
375
 
 
376
    def depart_option_argument(self, node):
 
377
        pass
 
378
 
 
379
    def visit_description(self, node):
 
380
        pass
 
381
    def depart_description(self, node):
 
382
        pass
 
383
 
 
384
    # Tables
 
385
    def visit_table(self, node):
 
386
        set_item_collector(node, 'table')
 
387
 
 
388
    def depart_table(self, node):
 
389
        node.parent.collect_text(node['table'])
 
390
 
 
391
    def visit_tgroup(self, node):
 
392
        set_item_list_collector(node, 'colspec')
 
393
        set_item_collector(node, 'head_entries')
 
394
        set_item_collector(node, 'body_rows')
 
395
 
 
396
    def depart_tgroup(self, node):
 
397
        header = []
 
398
        # The '@multitable {xxx}{xxx}' line
 
399
        self._decorate_list(node['colspec'], header.append,
 
400
                            '{%s}', '@multitable ', '\n')
 
401
        # The '@headitem xxx @tab yyy...' line
 
402
        head_entries = node['head_entries']
 
403
        if head_entries is not None:
 
404
            # Not all tables define titles for the columns... rest parser bug ?
 
405
            # FIXME: need a test
 
406
            self._decorate_list(head_entries[1:], header.append,
 
407
                                ' @tab %s',
 
408
                                '@headitem %s' % head_entries[0], '\n')
 
409
        header = ''.join(header)
 
410
        # The '@item xxx\n @tab yyy\n ...' lines
 
411
        body_rows = node['body_rows']
 
412
        rows = []
 
413
        for r in body_rows:
 
414
            self._decorate_list(r[1:], rows.append,
 
415
                                '@tab %s\n', '@item %s\n' % r[0])
 
416
        footer = '@end multitable\n'
 
417
        node.parent.collect_table(header + ''.join(rows) + footer)
 
418
 
 
419
    def visit_colspec(self, node):
 
420
        pass
 
421
 
 
422
    def depart_colspec(self, node):
 
423
        node.parent.collect_colspec('x' * node['colwidth'])
 
424
 
 
425
    def visit_thead(self, node):
 
426
        set_item_collector(node, 'row')
 
427
 
 
428
    def depart_thead(self, node):
 
429
        node.parent.collect_head_entries(node['row'])
 
430
 
 
431
    def visit_tbody(self, node):
 
432
        set_item_list_collector(node, 'row')
 
433
 
 
434
    def depart_tbody(self, node):
 
435
        node.parent.collect_body_rows(node['row'])
 
436
 
 
437
    def visit_row(self, node):
 
438
        set_item_list_collector(node, 'entry')
 
439
 
 
440
    def depart_row(self, node):
 
441
        node.parent.collect_row(node['entry'])
 
442
 
 
443
    def visit_entry(self, node):
 
444
        set_item_list_collector(node, 'text')
 
445
        node['par_sep_orig'] = self.paragraph_sep
 
446
        self.paragraph_sep = ''
 
447
 
 
448
    def depart_entry(self, node):
 
449
        node.parent.collect_entry(''.join(node['text']))
 
450
        self.paragraph_sep = node['par_sep_orig']
 
451
 
 
452
    # References
 
453
 
 
454
    def visit_reference(self, node):
 
455
        for c in node.children:
 
456
            if getattr(c, 'parent', None) is None:
 
457
                # Bug sphinx
 
458
                node.setup_child(c)
 
459
        set_item_collector(node, 'text')
 
460
 
 
461
    def depart_reference(self, node):
 
462
        anchorname = node.get('anchorname', None)
 
463
        refuri = node.get('refuri', None)
 
464
        refid = node.get('refid', None)
 
465
        text = ''.join(node['text'])
 
466
        collect = getattr(node.parent, 'collect_reference', None)
 
467
        if collect is not None:
 
468
            if not self.in_toctree:
 
469
                raise AssertionError('collect_reference is specific to toctree')
 
470
            if anchorname is None:
 
471
                anchorname = ''
 
472
            if refuri is None:
 
473
                refuri = ''
 
474
            collect((anchorname, refuri, text))
 
475
        elif refuri is not None:
 
476
            node.parent.collect_text('@uref{%s,%s}' % (refuri, text))
 
477
        elif refid is not None:
 
478
            # Info format requires that a reference is followed by some
 
479
            # punctuation char ('.', ','. ')', etc). Rest is more liberal. To
 
480
            # accommodate, we use pxref inside parenthesis.
 
481
            node.parent.collect_text('%s (@pxref{%s})' % (text, refid))
 
482
 
 
483
    def visit_footnote_reference(self, node):
 
484
        raise nodes.SkipNode # Not implemented yet
 
485
 
 
486
    def visit_citation_reference(self, node):
 
487
        raise nodes.SkipNode # Not implemented yet
 
488
 
 
489
    def visit_title_reference(self, node):
 
490
        raise nodes.SkipNode # Not implemented yet
 
491
 
 
492
    def depart_title_reference(self, node):
 
493
        pass
 
494
 
 
495
    def visit_target(self, node):
 
496
        raise nodes.SkipNode # Not implemented yet
 
497
 
 
498
    def depart_target(self, node):
 
499
        pass
 
500
 
 
501
    def visit_image(self, node):
 
502
        raise nodes.SkipNode # Not implemented yet
 
503
 
 
504
# Helpers to collect data in parent node
 
505
 
 
506
def set_item_collector(node, name):
 
507
    node[name] = None
 
508
    def set_item(item):
 
509
        node[name] = item
 
510
    setattr(node, 'collect_' + name, set_item)
 
511
 
 
512
 
 
513
def set_item_list_collector(node, name, sep=''):
 
514
    node[name] = []
 
515
    node[name + '_sep'] = sep
 
516
    def append_item(item):
 
517
        node[name].append(item)
 
518
    setattr(node, 'collect_' + name, append_item)
 
519
 
 
520