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