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