blob: b11b154421d56a15e45b9ff84eedba1a63677ccf [file] [log] [blame]
Tor Norbye3a2425a2013-11-04 10:16:08 -08001#
2# rst.py: ReStructuredText docstring parsing
3# Edward Loper
4#
5# Created [06/28/03 02:52 AM]
6# $Id: restructuredtext.py 1661 2007-11-07 12:59:34Z dvarrazzo $
7#
8
9"""
10Epydoc parser for ReStructuredText strings. ReStructuredText is the
11standard markup language used by the Docutils project.
12L{parse_docstring()} provides the primary interface to this module; it
13returns a L{ParsedRstDocstring}, which supports all of the methods
14defined by L{ParsedDocstring}.
15
16L{ParsedRstDocstring} is basically just a L{ParsedDocstring} wrapper
17for the C{docutils.nodes.document} class.
18
19Creating C{ParsedRstDocstring}s
20===============================
21
22C{ParsedRstDocstring}s are created by the C{parse_document} function,
23using the C{docutils.core.publish_string()} method, with the following
24helpers:
25
26 - An L{_EpydocReader} is used to capture all error messages as it
27 parses the docstring.
28 - A L{_DocumentPseudoWriter} is used to extract the document itself,
29 without actually writing any output. The document is saved for
30 further processing. The settings for the writer are copied from
31 C{docutils.writers.html4css1.Writer}, since those settings will
32 be used when we actually write the docstring to html.
33
34Using C{ParsedRstDocstring}s
35============================
36
37C{ParsedRstDocstring}s support all of the methods defined by
38C{ParsedDocstring}; but only the following four methods have
39non-default behavior:
40
41 - L{to_html()<ParsedRstDocstring.to_html>} uses an
42 L{_EpydocHTMLTranslator} to translate the C{ParsedRstDocstring}'s
43 document into an HTML segment.
44 - L{split_fields()<ParsedRstDocstring.split_fields>} uses a
45 L{_SplitFieldsTranslator} to divide the C{ParsedRstDocstring}'s
46 document into its main body and its fields. Special handling
47 is done to account for consolidated fields.
48 - L{summary()<ParsedRstDocstring.summary>} uses a
49 L{_SummaryExtractor} to extract the first sentence from
50 the C{ParsedRstDocstring}'s document.
51 - L{to_plaintext()<ParsedRstDocstring.to_plaintext>} uses
52 C{document.astext()} to convert the C{ParsedRstDocstring}'s
53 document to plaintext.
54
55@todo: Add ParsedRstDocstring.to_latex()
56@var CONSOLIDATED_FIELDS: A dictionary encoding the set of
57'consolidated fields' that can be used. Each consolidated field is
58marked by a single tag, and contains a single bulleted list, where
59each list item starts with an identifier, marked as interpreted text
60(C{`...`}). This module automatically splits these consolidated
61fields into individual fields. The keys of C{CONSOLIDATED_FIELDS} are
62the names of possible consolidated fields; and the values are the
63names of the field tags that should be used for individual entries in
64the list.
65"""
66__docformat__ = 'epytext en'
67
68# Imports
69import re, os, os.path
70from xml.dom.minidom import *
71
72from docutils.core import publish_string
73from docutils.writers import Writer
74from docutils.writers.html4css1 import HTMLTranslator, Writer as HTMLWriter
75from docutils.writers.latex2e import LaTeXTranslator, Writer as LaTeXWriter
76from docutils.readers.standalone import Reader as StandaloneReader
77from docutils.utils import new_document
78from docutils.nodes import NodeVisitor, Text, SkipChildren
79from docutils.nodes import SkipNode, TreeCopyVisitor
80from docutils.frontend import OptionParser
81from docutils.parsers.rst import directives, roles
82import docutils.nodes
83import docutils.transforms.frontmatter
84import docutils.transforms
85import docutils.utils
86
87from epydoc.compat import * # Backwards compatibility
88from epydoc.markup import *
89from epydoc.apidoc import ModuleDoc, ClassDoc
90from epydoc.docwriter.dotgraph import *
91from epydoc.docwriter.xlink import ApiLinkReader
92from epydoc.markup.doctest import doctest_to_html, doctest_to_latex, \
93 HTMLDoctestColorizer
94
95#: A dictionary whose keys are the "consolidated fields" that are
96#: recognized by epydoc; and whose values are the corresponding epydoc
97#: field names that should be used for the individual fields.
98CONSOLIDATED_FIELDS = {
99 'parameters': 'param',
100 'arguments': 'arg',
101 'exceptions': 'except',
102 'variables': 'var',
103 'ivariables': 'ivar',
104 'cvariables': 'cvar',
105 'groups': 'group',
106 'types': 'type',
107 'keywords': 'keyword',
108 }
109
110#: A list of consolidated fields whose bodies may be specified using a
111#: definition list, rather than a bulleted list. For these fields, the
112#: 'classifier' for each term in the definition list is translated into
113#: a @type field.
114CONSOLIDATED_DEFLIST_FIELDS = ['param', 'arg', 'var', 'ivar', 'cvar', 'keyword']
115
116def parse_docstring(docstring, errors, **options):
117 """
118 Parse the given docstring, which is formatted using
119 ReStructuredText; and return a L{ParsedDocstring} representation
120 of its contents.
121 @param docstring: The docstring to parse
122 @type docstring: C{string}
123 @param errors: A list where any errors generated during parsing
124 will be stored.
125 @type errors: C{list} of L{ParseError}
126 @param options: Extra options. Unknown options are ignored.
127 Currently, no extra options are defined.
128 @rtype: L{ParsedDocstring}
129 """
130 writer = _DocumentPseudoWriter()
131 reader = _EpydocReader(errors) # Outputs errors to the list.
132 publish_string(docstring, writer=writer, reader=reader,
133 settings_overrides={'report_level':10000,
134 'halt_level':10000,
135 'warning_stream':None})
136 return ParsedRstDocstring(writer.document)
137
138class OptimizedReporter(docutils.utils.Reporter):
139 """A reporter that ignores all debug messages. This is used to
140 shave a couple seconds off of epydoc's run time, since docutils
141 isn't very fast about processing its own debug messages."""
142 def debug(self, *args, **kwargs): pass
143
144class ParsedRstDocstring(ParsedDocstring):
145 """
146 An encoded version of a ReStructuredText docstring. The contents
147 of the docstring are encoded in the L{_document} instance
148 variable.
149
150 @ivar _document: A ReStructuredText document, encoding the
151 docstring.
152 @type _document: C{docutils.nodes.document}
153 """
154 def __init__(self, document):
155 """
156 @type document: C{docutils.nodes.document}
157 """
158 self._document = document
159
160 # The default document reporter and transformer are not
161 # pickle-able; so replace them with stubs that are.
162 document.reporter = OptimizedReporter(
163 document.reporter.source, 'SEVERE', 'SEVERE', '')
164 document.transformer = docutils.transforms.Transformer(document)
165
166 def split_fields(self, errors=None):
167 # Inherit docs
168 if errors is None: errors = []
169 visitor = _SplitFieldsTranslator(self._document, errors)
170 self._document.walk(visitor)
171 if len(self._document.children) > 0:
172 return self, visitor.fields
173 else:
174 return None, visitor.fields
175
176 def summary(self):
177 # Inherit docs
178 visitor = _SummaryExtractor(self._document)
179 try: self._document.walk(visitor)
180 except docutils.nodes.NodeFound: pass
181 return visitor.summary, bool(visitor.other_docs)
182
183# def concatenate(self, other):
184# result = self._document.copy()
185# for child in (self._document.get_children() +
186# other._document.get_children()):
187# visitor = TreeCopyVisitor(self._document)
188# child.walkabout(visitor)
189# result.append(visitor.get_tree_copy())
190# return ParsedRstDocstring(result)
191
192 def to_html(self, docstring_linker, directory=None,
193 docindex=None, context=None, **options):
194 # Inherit docs
195 visitor = _EpydocHTMLTranslator(self._document, docstring_linker,
196 directory, docindex, context)
197 self._document.walkabout(visitor)
198 return ''.join(visitor.body)
199
200 def to_latex(self, docstring_linker, **options):
201 # Inherit docs
202 visitor = _EpydocLaTeXTranslator(self._document, docstring_linker)
203 self._document.walkabout(visitor)
204 return ''.join(visitor.body)
205
206 def to_plaintext(self, docstring_linker, **options):
207 # This is should be replaced by something better:
208 return self._document.astext()
209
210 def __repr__(self): return '<ParsedRstDocstring: ...>'
211
212 def index_terms(self):
213 visitor = _TermsExtractor(self._document)
214 self._document.walkabout(visitor)
215 return visitor.terms
216
217class _EpydocReader(ApiLinkReader):
218 """
219 A reader that captures all errors that are generated by parsing,
220 and appends them to a list.
221 """
222 # Remove the DocInfo transform, to ensure that :author: fields are
223 # correctly handled. This needs to be handled differently
224 # depending on the version of docutils that's being used, because
225 # the default_transforms attribute was deprecated & replaced by
226 # get_transforms().
227 version = [int(v) for v in docutils.__version__.split('.')]
228 version += [ 0 ] * (3 - len(version))
229 if version < [0,4,0]:
230 default_transforms = list(ApiLinkReader.default_transforms)
231 try: default_transforms.remove(docutils.transforms.frontmatter.DocInfo)
232 except ValueError: pass
233 else:
234 def get_transforms(self):
235 return [t for t in ApiLinkReader.get_transforms(self)
236 if t != docutils.transforms.frontmatter.DocInfo]
237 del version
238
239 def __init__(self, errors):
240 self._errors = errors
241 ApiLinkReader.__init__(self)
242
243 def new_document(self):
244 document = new_document(self.source.source_path, self.settings)
245 # Capture all warning messages.
246 document.reporter.attach_observer(self.report)
247 # These are used so we know how to encode warning messages:
248 self._encoding = document.reporter.encoding
249 self._error_handler = document.reporter.error_handler
250 # Return the new document.
251 return document
252
253 def report(self, error):
254 try: is_fatal = int(error['level']) > 2
255 except: is_fatal = 1
256 try: linenum = int(error['line'])
257 except: linenum = None
258
259 msg = ''.join([c.astext().encode(self._encoding, self._error_handler)
260 for c in error])
261
262 self._errors.append(ParseError(msg, linenum, is_fatal))
263
264class _DocumentPseudoWriter(Writer):
265 """
266 A pseudo-writer for the docutils framework, that can be used to
267 access the document itself. The output of C{_DocumentPseudoWriter}
268 is just an empty string; but after it has been used, the most
269 recently processed document is available as the instance variable
270 C{document}
271
272 @type document: C{docutils.nodes.document}
273 @ivar document: The most recently processed document.
274 """
275 def __init__(self):
276 self.document = None
277 Writer.__init__(self)
278
279 def translate(self):
280 self.output = ''
281
282class _SummaryExtractor(NodeVisitor):
283 """
284 A docutils node visitor that extracts the first sentence from
285 the first paragraph in a document.
286 """
287 def __init__(self, document):
288 NodeVisitor.__init__(self, document)
289 self.summary = None
290 self.other_docs = None
291
292 def visit_document(self, node):
293 self.summary = None
294
295 _SUMMARY_RE = re.compile(r'(\s*[\w\W]*?\.)(\s|$)')
296 def visit_paragraph(self, node):
297 if self.summary is not None:
298 # found a paragraph after the first one
299 self.other_docs = True
300 raise docutils.nodes.NodeFound('Found summary')
301
302 summary_pieces = []
303
304 # Extract the first sentence.
305 for child in node:
306 if isinstance(child, docutils.nodes.Text):
307 m = self._SUMMARY_RE.match(child.data)
308 if m:
309 summary_pieces.append(docutils.nodes.Text(m.group(1)))
310 other = child.data[m.end():]
311 if other and not other.isspace():
312 self.other_docs = True
313 break
314 summary_pieces.append(child)
315
316 summary_doc = self.document.copy() # shallow copy
317 summary_para = node.copy() # shallow copy
318 summary_doc[:] = [summary_para]
319 summary_para[:] = summary_pieces
320 self.summary = ParsedRstDocstring(summary_doc)
321
322 def visit_field(self, node):
323 raise SkipNode
324
325 def unknown_visit(self, node):
326 'Ignore all unknown nodes'
327
328class _TermsExtractor(NodeVisitor):
329 """
330 A docutils node visitor that extracts the terms from documentation.
331
332 Terms are created using the C{:term:} interpreted text role.
333 """
334 def __init__(self, document):
335 NodeVisitor.__init__(self, document)
336
337 self.terms = None
338 """
339 The terms currently found.
340 @type: C{list}
341 """
342
343 def visit_document(self, node):
344 self.terms = []
345 self._in_term = False
346
347 def visit_emphasis(self, node):
348 if 'term' in node.get('classes'):
349 self._in_term = True
350
351 def depart_emphasis(self, node):
352 if 'term' in node.get('classes'):
353 self._in_term = False
354
355 def visit_Text(self, node):
356 if self._in_term:
357 doc = self.document.copy()
358 doc[:] = [node.copy()]
359 self.terms.append(ParsedRstDocstring(doc))
360
361 def unknown_visit(self, node):
362 'Ignore all unknown nodes'
363
364 def unknown_departure(self, node):
365 'Ignore all unknown nodes'
366
367class _SplitFieldsTranslator(NodeVisitor):
368 """
369 A docutils translator that removes all fields from a document, and
370 collects them into the instance variable C{fields}
371
372 @ivar fields: The fields of the most recently walked document.
373 @type fields: C{list} of L{Field<markup.Field>}
374 """
375
376 ALLOW_UNMARKED_ARG_IN_CONSOLIDATED_FIELD = True
377 """If true, then consolidated fields are not required to mark
378 arguments with C{`backticks`}. (This is currently only
379 implemented for consolidated fields expressed as definition lists;
380 consolidated fields expressed as unordered lists still require
381 backticks for now."""
382
383 def __init__(self, document, errors):
384 NodeVisitor.__init__(self, document)
385 self._errors = errors
386 self.fields = []
387 self._newfields = {}
388
389 def visit_document(self, node):
390 self.fields = []
391
392 def visit_field(self, node):
393 # Remove the field from the tree.
394 node.parent.remove(node)
395
396 # Extract the field name & optional argument
397 tag = node[0].astext().split(None, 1)
398 tagname = tag[0]
399 if len(tag)>1: arg = tag[1]
400 else: arg = None
401
402 # Handle special fields:
403 fbody = node[1]
404 if arg is None:
405 for (list_tag, entry_tag) in CONSOLIDATED_FIELDS.items():
406 if tagname.lower() == list_tag:
407 try:
408 self.handle_consolidated_field(fbody, entry_tag)
409 return
410 except ValueError, e:
411 estr = 'Unable to split consolidated field '
412 estr += '"%s" - %s' % (tagname, e)
413 self._errors.append(ParseError(estr, node.line,
414 is_fatal=0))
415
416 # Use a @newfield to let it be displayed as-is.
417 if tagname.lower() not in self._newfields:
418 newfield = Field('newfield', tagname.lower(),
419 parse(tagname, 'plaintext'))
420 self.fields.append(newfield)
421 self._newfields[tagname.lower()] = 1
422
423 self._add_field(tagname, arg, fbody)
424
425 def _add_field(self, tagname, arg, fbody):
426 field_doc = self.document.copy()
427 for child in fbody: field_doc.append(child)
428 field_pdoc = ParsedRstDocstring(field_doc)
429 self.fields.append(Field(tagname, arg, field_pdoc))
430
431 def visit_field_list(self, node):
432 # Remove the field list from the tree. The visitor will still walk
433 # over the node's children.
434 node.parent.remove(node)
435
436 def handle_consolidated_field(self, body, tagname):
437 """
438 Attempt to handle a consolidated section.
439 """
440 if len(body) != 1:
441 raise ValueError('does not contain a single list.')
442 elif body[0].tagname == 'bullet_list':
443 self.handle_consolidated_bullet_list(body[0], tagname)
444 elif (body[0].tagname == 'definition_list' and
445 tagname in CONSOLIDATED_DEFLIST_FIELDS):
446 self.handle_consolidated_definition_list(body[0], tagname)
447 elif tagname in CONSOLIDATED_DEFLIST_FIELDS:
448 raise ValueError('does not contain a bulleted list or '
449 'definition list.')
450 else:
451 raise ValueError('does not contain a bulleted list.')
452
453 def handle_consolidated_bullet_list(self, items, tagname):
454 # Check the contents of the list. In particular, each list
455 # item should have the form:
456 # - `arg`: description...
457 n = 0
458 _BAD_ITEM = ("list item %d is not well formed. Each item must "
459 "consist of a single marked identifier (e.g., `x`), "
460 "optionally followed by a colon or dash and a "
461 "description.")
462 for item in items:
463 n += 1
464 if item.tagname != 'list_item' or len(item) == 0:
465 raise ValueError('bad bulleted list (bad child %d).' % n)
466 if item[0].tagname != 'paragraph':
467 if item[0].tagname == 'definition_list':
468 raise ValueError(('list item %d contains a definition '+
469 'list (it\'s probably indented '+
470 'wrong).') % n)
471 else:
472 raise ValueError(_BAD_ITEM % n)
473 if len(item[0]) == 0:
474 raise ValueError(_BAD_ITEM % n)
475 if item[0][0].tagname != 'title_reference':
476 raise ValueError(_BAD_ITEM % n)
477
478 # Everything looks good; convert to multiple fields.
479 for item in items:
480 # Extract the arg
481 arg = item[0][0].astext()
482
483 # Extract the field body, and remove the arg
484 fbody = item[:]
485 fbody[0] = fbody[0].copy()
486 fbody[0][:] = item[0][1:]
487
488 # Remove the separating ":", if present
489 if (len(fbody[0]) > 0 and
490 isinstance(fbody[0][0], docutils.nodes.Text)):
491 child = fbody[0][0]
492 if child.data[:1] in ':-':
493 child.data = child.data[1:].lstrip()
494 elif child.data[:2] in (' -', ' :'):
495 child.data = child.data[2:].lstrip()
496
497 # Wrap the field body, and add a new field
498 self._add_field(tagname, arg, fbody)
499
500 def handle_consolidated_definition_list(self, items, tagname):
501 # Check the list contents.
502 n = 0
503 _BAD_ITEM = ("item %d is not well formed. Each item's term must "
504 "consist of a single marked identifier (e.g., `x`), "
505 "optionally followed by a space, colon, space, and "
506 "a type description.")
507 for item in items:
508 n += 1
509 if (item.tagname != 'definition_list_item' or len(item) < 2 or
510 item[0].tagname != 'term' or
511 item[-1].tagname != 'definition'):
512 raise ValueError('bad definition list (bad child %d).' % n)
513 if len(item) > 3:
514 raise ValueError(_BAD_ITEM % n)
515 if not ((item[0][0].tagname == 'title_reference') or
516 (self.ALLOW_UNMARKED_ARG_IN_CONSOLIDATED_FIELD and
517 isinstance(item[0][0], docutils.nodes.Text))):
518 raise ValueError(_BAD_ITEM % n)
519 for child in item[0][1:]:
520 if child.astext() != '':
521 raise ValueError(_BAD_ITEM % n)
522
523 # Extract it.
524 for item in items:
525 # The basic field.
526 arg = item[0][0].astext()
527 fbody = item[-1]
528 self._add_field(tagname, arg, fbody)
529 # If there's a classifier, treat it as a type.
530 if len(item) == 3:
531 type_descr = item[1]
532 self._add_field('type', arg, type_descr)
533
534 def unknown_visit(self, node):
535 'Ignore all unknown nodes'
536
537def latex_head_prefix():
538 document = new_document('<fake>')
539 translator = _EpydocLaTeXTranslator(document, None)
540 return translator.head_prefix
541
542class _EpydocLaTeXTranslator(LaTeXTranslator):
543 settings = None
544 def __init__(self, document, docstring_linker):
545 # Set the document's settings.
546 if self.settings is None:
547 settings = OptionParser([LaTeXWriter()]).get_default_values()
548 settings.output_encoding = 'utf-8'
549 self.__class__.settings = settings
550 document.settings = self.settings
551
552 LaTeXTranslator.__init__(self, document)
553 self._linker = docstring_linker
554
555 # Start at section level 3. (Unfortunately, we now have to
556 # set a private variable to make this work; perhaps the standard
557 # latex translator should grow an official way to spell this?)
558 self.section_level = 3
559 self._section_number = [0]*self.section_level
560
561 # Handle interpreted text (crossreferences)
562 def visit_title_reference(self, node):
563 target = self.encode(node.astext())
564 xref = self._linker.translate_identifier_xref(target, target)
565 self.body.append(xref)
566 raise SkipNode()
567
568 def visit_document(self, node): pass
569 def depart_document(self, node): pass
570
571 # For now, just ignore dotgraphs. [XXX]
572 def visit_dotgraph(self, node):
573 log.warning("Ignoring dotgraph in latex output (dotgraph "
574 "rendering for latex not implemented yet).")
575 raise SkipNode()
576
577 def visit_doctest_block(self, node):
578 self.body.append(doctest_to_latex(node[0].astext()))
579 raise SkipNode()
580
581class _EpydocHTMLTranslator(HTMLTranslator):
582 settings = None
583 def __init__(self, document, docstring_linker, directory,
584 docindex, context):
585 self._linker = docstring_linker
586 self._directory = directory
587 self._docindex = docindex
588 self._context = context
589
590 # Set the document's settings.
591 if self.settings is None:
592 settings = OptionParser([HTMLWriter()]).get_default_values()
593 self.__class__.settings = settings
594 document.settings = self.settings
595
596 # Call the parent constructor.
597 HTMLTranslator.__init__(self, document)
598
599 # Handle interpreted text (crossreferences)
600 def visit_title_reference(self, node):
601 target = self.encode(node.astext())
602 xref = self._linker.translate_identifier_xref(target, target)
603 self.body.append(xref)
604 raise SkipNode()
605
606 def should_be_compact_paragraph(self, node):
607 if self.document.children == [node]:
608 return True
609 else:
610 return HTMLTranslator.should_be_compact_paragraph(self, node)
611
612 def visit_document(self, node): pass
613 def depart_document(self, node): pass
614
615 def starttag(self, node, tagname, suffix='\n', **attributes):
616 """
617 This modified version of starttag makes a few changes to HTML
618 tags, to prevent them from conflicting with epydoc. In particular:
619 - existing class attributes are prefixed with C{'rst-'}
620 - existing names are prefixed with C{'rst-'}
621 - hrefs starting with C{'#'} are prefixed with C{'rst-'}
622 - hrefs not starting with C{'#'} are given target='_top'
623 - all headings (C{<hM{n}>}) are given the css class C{'heading'}
624 """
625 # Get the list of all attribute dictionaries we need to munge.
626 attr_dicts = [attributes]
627 if isinstance(node, docutils.nodes.Node):
628 attr_dicts.append(node.attributes)
629 if isinstance(node, dict):
630 attr_dicts.append(node)
631 # Munge each attribute dictionary. Unfortunately, we need to
632 # iterate through attributes one at a time because some
633 # versions of docutils don't case-normalize attributes.
634 for attr_dict in attr_dicts:
635 for (key, val) in attr_dict.items():
636 # Prefix all CSS classes with "rst-"; and prefix all
637 # names with "rst-" to avoid conflicts.
638 if key.lower() in ('class', 'id', 'name'):
639 attr_dict[key] = 'rst-%s' % val
640 elif key.lower() in ('classes', 'ids', 'names'):
641 attr_dict[key] = ['rst-%s' % cls for cls in val]
642 elif key.lower() == 'href':
643 if attr_dict[key][:1]=='#':
644 attr_dict[key] = '#rst-%s' % attr_dict[key][1:]
645 else:
646 # If it's an external link, open it in a new
647 # page.
648 attr_dict['target'] = '_top'
649
650 # For headings, use class="heading"
651 if re.match(r'^h\d+$', tagname):
652 attributes['class'] = ' '.join([attributes.get('class',''),
653 'heading']).strip()
654
655 return HTMLTranslator.starttag(self, node, tagname, suffix,
656 **attributes)
657
658 def visit_dotgraph(self, node):
659 if self._directory is None: return # [xx] warning?
660
661 # Generate the graph.
662 graph = node.graph(self._docindex, self._context, self._linker)
663 if graph is None: return
664
665 # Write the graph.
666 image_url = '%s.gif' % graph.uid
667 image_file = os.path.join(self._directory, image_url)
668 self.body.append(graph.to_html(image_file, image_url))
669 raise SkipNode()
670
671 def visit_doctest_block(self, node):
672 pysrc = node[0].astext()
673 if node.get('codeblock'):
674 self.body.append(HTMLDoctestColorizer().colorize_codeblock(pysrc))
675 else:
676 self.body.append(doctest_to_html(pysrc))
677 raise SkipNode()
678
679 def visit_emphasis(self, node):
680 # Generate a corrent index term anchor
681 if 'term' in node.get('classes') and node.children:
682 doc = self.document.copy()
683 doc[:] = [node.children[0].copy()]
684 self.body.append(
685 self._linker.translate_indexterm(ParsedRstDocstring(doc)))
686 raise SkipNode()
687
688 HTMLTranslator.visit_emphasis(self, node)
689
690def python_code_directive(name, arguments, options, content, lineno,
691 content_offset, block_text, state, state_machine):
692 """
693 A custom restructuredtext directive which can be used to display
694 syntax-highlighted Python code blocks. This directive takes no
695 arguments, and the body should contain only Python code. This
696 directive can be used instead of doctest blocks when it is
697 inconvenient to list prompts on each line, or when you would
698 prefer that the output not contain prompts (e.g., to make
699 copy/paste easier).
700 """
701 required_arguments = 0
702 optional_arguments = 0
703
704 text = '\n'.join(content)
705 node = docutils.nodes.doctest_block(text, text, codeblock=True)
706 return [ node ]
707
708python_code_directive.arguments = (0, 0, 0)
709python_code_directive.content = True
710
711directives.register_directive('python', python_code_directive)
712
713def term_role(name, rawtext, text, lineno, inliner,
714 options={}, content=[]):
715
716 text = docutils.utils.unescape(text)
717 node = docutils.nodes.emphasis(rawtext, text, **options)
718 node.attributes['classes'].append('term')
719
720 return [node], []
721
722roles.register_local_role('term', term_role)
723
724######################################################################
725#{ Graph Generation Directives
726######################################################################
727# See http://docutils.sourceforge.net/docs/howto/rst-directives.html
728
729class dotgraph(docutils.nodes.image):
730 """
731 A custom docutils node that should be rendered using Graphviz dot.
732 This node does not directly store the graph; instead, it stores a
733 pointer to a function that can be used to generate the graph.
734 This allows the graph to be built based on information that might
735 not be available yet at parse time. This graph generation
736 function has the following signature:
737
738 >>> def generate_graph(docindex, context, linker, *args):
739 ... 'generates and returns a new DotGraph'
740
741 Where C{docindex} is a docindex containing the documentation that
742 epydoc has built; C{context} is the C{APIDoc} whose docstring
743 contains this dotgraph node; C{linker} is a L{DocstringLinker}
744 that can be used to resolve crossreferences; and C{args} is any
745 extra arguments that are passed to the C{dotgraph} constructor.
746 """
747 def __init__(self, generate_graph_func, *generate_graph_args):
748 docutils.nodes.image.__init__(self)
749 self.graph_func = generate_graph_func
750 self.args = generate_graph_args
751 def graph(self, docindex, context, linker):
752 return self.graph_func(docindex, context, linker, *self.args)
753
754def _dir_option(argument):
755 """A directive option spec for the orientation of a graph."""
756 argument = argument.lower().strip()
757 if argument == 'right': return 'LR'
758 if argument == 'left': return 'RL'
759 if argument == 'down': return 'TB'
760 if argument == 'up': return 'BT'
761 raise ValueError('%r unknown; choose from left, right, up, down' %
762 argument)
763
764def digraph_directive(name, arguments, options, content, lineno,
765 content_offset, block_text, state, state_machine):
766 """
767 A custom restructuredtext directive which can be used to display
768 Graphviz dot graphs. This directive takes a single argument,
769 which is used as the graph's name. The contents of the directive
770 are used as the body of the graph. Any href attributes whose
771 value has the form <name> will be replaced by the URL of the object
772 with that name. Here's a simple example::
773
774 .. digraph:: example_digraph
775 a -> b -> c
776 c -> a [dir=\"none\"]
777 """
778 if arguments: title = arguments[0]
779 else: title = ''
780 return [ dotgraph(_construct_digraph, title, options.get('caption'),
781 '\n'.join(content)) ]
782digraph_directive.arguments = (0, 1, True)
783digraph_directive.options = {'caption': directives.unchanged}
784digraph_directive.content = True
785directives.register_directive('digraph', digraph_directive)
786
787def _construct_digraph(docindex, context, linker, title, caption,
788 body):
789 """Graph generator for L{digraph_directive}"""
790 graph = DotGraph(title, body, caption=caption)
791 graph.link(linker)
792 return graph
793
794def classtree_directive(name, arguments, options, content, lineno,
795 content_offset, block_text, state, state_machine):
796 """
797 A custom restructuredtext directive which can be used to
798 graphically display a class hierarchy. If one or more arguments
799 are given, then those classes and all their descendants will be
800 displayed. If no arguments are given, and the directive is in a
801 class's docstring, then that class and all its descendants will be
802 displayed. It is an error to use this directive with no arguments
803 in a non-class docstring.
804
805 Options:
806 - C{:dir:} -- Specifies the orientation of the graph. One of
807 C{down}, C{right} (default), C{left}, C{up}.
808 """
809 return [ dotgraph(_construct_classtree, arguments, options) ]
810classtree_directive.arguments = (0, 1, True)
811classtree_directive.options = {'dir': _dir_option}
812classtree_directive.content = False
813directives.register_directive('classtree', classtree_directive)
814
815def _construct_classtree(docindex, context, linker, arguments, options):
816 """Graph generator for L{classtree_directive}"""
817 if len(arguments) == 1:
818 bases = [docindex.find(name, context) for name in
819 arguments[0].replace(',',' ').split()]
820 bases = [d for d in bases if isinstance(d, ClassDoc)]
821 elif isinstance(context, ClassDoc):
822 bases = [context]
823 else:
824 log.warning("Could not construct class tree: you must "
825 "specify one or more base classes.")
826 return None
827
828 return class_tree_graph(bases, linker, context, **options)
829
830def packagetree_directive(name, arguments, options, content, lineno,
831 content_offset, block_text, state, state_machine):
832 """
833 A custom restructuredtext directive which can be used to
834 graphically display a package hierarchy. If one or more arguments
835 are given, then those packages and all their submodules will be
836 displayed. If no arguments are given, and the directive is in a
837 package's docstring, then that package and all its submodules will
838 be displayed. It is an error to use this directive with no
839 arguments in a non-package docstring.
840
841 Options:
842 - C{:dir:} -- Specifies the orientation of the graph. One of
843 C{down}, C{right} (default), C{left}, C{up}.
844 """
845 return [ dotgraph(_construct_packagetree, arguments, options) ]
846packagetree_directive.arguments = (0, 1, True)
847packagetree_directive.options = {
848 'dir': _dir_option,
849 'style': lambda a:directives.choice(a.lower(), ('uml', 'tree'))}
850packagetree_directive.content = False
851directives.register_directive('packagetree', packagetree_directive)
852
853def _construct_packagetree(docindex, context, linker, arguments, options):
854 """Graph generator for L{packagetree_directive}"""
855 if len(arguments) == 1:
856 packages = [docindex.find(name, context) for name in
857 arguments[0].replace(',',' ').split()]
858 packages = [d for d in packages if isinstance(d, ModuleDoc)]
859 elif isinstance(context, ModuleDoc):
860 packages = [context]
861 else:
862 log.warning("Could not construct package tree: you must "
863 "specify one or more root packages.")
864 return None
865
866 return package_tree_graph(packages, linker, context, **options)
867
868def importgraph_directive(name, arguments, options, content, lineno,
869 content_offset, block_text, state, state_machine):
870 return [ dotgraph(_construct_importgraph, arguments, options) ]
871importgraph_directive.arguments = (0, 1, True)
872importgraph_directive.options = {'dir': _dir_option}
873importgraph_directive.content = False
874directives.register_directive('importgraph', importgraph_directive)
875
876def _construct_importgraph(docindex, context, linker, arguments, options):
877 """Graph generator for L{importgraph_directive}"""
878 if len(arguments) == 1:
879 modules = [ docindex.find(name, context)
880 for name in arguments[0].replace(',',' ').split() ]
881 modules = [d for d in modules if isinstance(d, ModuleDoc)]
882 else:
883 modules = [d for d in docindex.root if isinstance(d, ModuleDoc)]
884
885 return import_graph(modules, docindex, linker, context, **options)
886
887def callgraph_directive(name, arguments, options, content, lineno,
888 content_offset, block_text, state, state_machine):
889 return [ dotgraph(_construct_callgraph, arguments, options) ]
890callgraph_directive.arguments = (0, 1, True)
891callgraph_directive.options = {'dir': _dir_option,
892 'add_callers': directives.flag,
893 'add_callees': directives.flag}
894callgraph_directive.content = False
895directives.register_directive('callgraph', callgraph_directive)
896
897def _construct_callgraph(docindex, context, linker, arguments, options):
898 """Graph generator for L{callgraph_directive}"""
899 if len(arguments) == 1:
900 docs = [docindex.find(name, context) for name in
901 arguments[0].replace(',',' ').split()]
902 docs = [doc for doc in docs if doc is not None]
903 else:
904 docs = [context]
905 return call_graph(docs, docindex, linker, context, **options)
906