| Tor Norbye | 3a2425a | 2013-11-04 10:16:08 -0800 | [diff] [blame^] | 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 | |