Package epydoc :: Package markup :: Module restructuredtext
[hide private]
[frames] | no frames]

Source Code for Module epydoc.markup.restructuredtext

  1  # 
  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  """ 
 10  Epydoc parser for ReStructuredText strings.  ReStructuredText is the 
 11  standard markup language used by the Docutils project. 
 12  L{parse_docstring()} provides the primary interface to this module; it 
 13  returns a L{ParsedRstDocstring}, which supports all of the methods 
 14  defined by L{ParsedDocstring}. 
 15   
 16  L{ParsedRstDocstring} is basically just a L{ParsedDocstring} wrapper 
 17  for the C{docutils.nodes.document} class. 
 18   
 19  Creating C{ParsedRstDocstring}s 
 20  =============================== 
 21   
 22  C{ParsedRstDocstring}s are created by the C{parse_document} function, 
 23  using the C{docutils.core.publish_string()} method, with the following 
 24  helpers: 
 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   
 34  Using C{ParsedRstDocstring}s 
 35  ============================ 
 36   
 37  C{ParsedRstDocstring}s support all of the methods defined by 
 38  C{ParsedDocstring}; but only the following four methods have 
 39  non-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 
 58  marked by a single tag, and contains a single bulleted list, where 
 59  each list item starts with an identifier, marked as interpreted text 
 60  (C{`...`}).  This module automatically splits these consolidated 
 61  fields into individual fields.  The keys of C{CONSOLIDATED_FIELDS} are 
 62  the names of possible consolidated fields; and the values are the 
 63  names of the field tags that should be used for individual entries in 
 64  the list. 
 65  """ 
 66  __docformat__ = 'epytext en' 
 67   
 68  # Imports 
 69  import re, os, os.path 
 70  from xml.dom.minidom import * 
 71   
 72  from docutils.core import publish_string 
 73  from docutils.writers import Writer 
 74  from docutils.writers.html4css1 import HTMLTranslator, Writer as HTMLWriter 
 75  from docutils.writers.latex2e import LaTeXTranslator, Writer as LaTeXWriter 
 76  from docutils.readers.standalone import Reader as StandaloneReader 
 77  from docutils.utils import new_document 
 78  from docutils.nodes import NodeVisitor, Text, SkipChildren 
 79  from docutils.nodes import SkipNode, TreeCopyVisitor 
 80  from docutils.frontend import OptionParser 
 81  from docutils.parsers.rst import directives, roles 
 82  import docutils.nodes 
 83  import docutils.transforms.frontmatter 
 84  import docutils.transforms 
 85  import docutils.utils 
 86   
 87  from epydoc.compat import * # Backwards compatibility 
 88  from epydoc.markup import * 
 89  from epydoc.apidoc import ModuleDoc, ClassDoc 
 90  from epydoc.docwriter.dotgraph import * 
 91  from epydoc.docwriter.xlink import ApiLinkReader 
 92  from 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. 
 98  CONSOLIDATED_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. 
114  CONSOLIDATED_DEFLIST_FIELDS = ['param', 'arg', 'var', 'ivar', 'cvar', 'keyword'] 
115   
116 -def 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
138 -class 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
144 -class 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
217 -class _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
264 -class _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
282 -class _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
328 -class _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
367 -class _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
537 -def latex_head_prefix():
538 document = new_document('<fake>') 539 translator = _EpydocLaTeXTranslator(document, None) 540 return translator.head_prefix
541
542 -class _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
581 -class _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
690 -def 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 708 python_code_directive.arguments = (0, 0, 0) 709 python_code_directive.content = True 710 711 directives.register_directive('python', python_code_directive) 712
713 -def 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 722 roles.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
729 -class 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
754 -def _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
764 -def 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)) ]
782 digraph_directive.arguments = (0, 1, True) 783 digraph_directive.options = {'caption': directives.unchanged} 784 digraph_directive.content = True 785 directives.register_directive('digraph', digraph_directive) 786
787 -def _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
794 -def 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) ]
810 classtree_directive.arguments = (0, 1, True) 811 classtree_directive.options = {'dir': _dir_option} 812 classtree_directive.content = False 813 directives.register_directive('classtree', classtree_directive) 814
815 -def _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
830 -def 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) ]
846 packagetree_directive.arguments = (0, 1, True) 847 packagetree_directive.options = { 848 'dir': _dir_option, 849 'style': lambda a:directives.choice(a.lower(), ('uml', 'tree'))} 850 packagetree_directive.content = False 851 directives.register_directive('packagetree', packagetree_directive) 852
853 -def _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
868 -def importgraph_directive(name, arguments, options, content, lineno, 869 content_offset, block_text, state, state_machine):
870 return [ dotgraph(_construct_importgraph, arguments, options) ]
871 importgraph_directive.arguments = (0, 1, True) 872 importgraph_directive.options = {'dir': _dir_option} 873 importgraph_directive.content = False 874 directives.register_directive('importgraph', importgraph_directive) 875
876 -def _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
887 -def callgraph_directive(name, arguments, options, content, lineno, 888 content_offset, block_text, state, state_machine):
889 return [ dotgraph(_construct_callgraph, arguments, options) ]
890 callgraph_directive.arguments = (0, 1, True) 891 callgraph_directive.options = {'dir': _dir_option, 892 'add_callers': directives.flag, 893 'add_callees': directives.flag} 894 callgraph_directive.content = False 895 directives.register_directive('callgraph', callgraph_directive) 896
897 -def _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