Home | History | Annotate | Download | only in extensions
      1 # -*- coding: utf-8 -*-
      2 """
      3     pyspecific.py
      4     ~~~~~~~~~~~~~
      5 
      6     Sphinx extension with Python doc-specific markup.
      7 
      8     :copyright: 2008-2014 by Georg Brandl.
      9     :license: Python license.
     10 """
     11 
     12 import re
     13 import codecs
     14 from os import path
     15 from time import asctime
     16 from pprint import pformat
     17 from docutils.io import StringOutput
     18 from docutils.utils import new_document
     19 
     20 from docutils import nodes, utils
     21 
     22 from sphinx import addnodes
     23 from sphinx.builders import Builder
     24 from sphinx.util.nodes import split_explicit_title
     25 from sphinx.util.compat import Directive
     26 from sphinx.writers.html import HTMLTranslator
     27 from sphinx.writers.text import TextWriter
     28 from sphinx.writers.latex import LaTeXTranslator
     29 from sphinx.domains.python import PyModulelevel, PyClassmember
     30 
     31 # Support for checking for suspicious markup
     32 
     33 import suspicious
     34 
     35 
     36 ISSUE_URI = 'https://bugs.python.org/issue%s'
     37 SOURCE_URI = 'https://github.com/python/cpython/tree/3.6/%s'
     38 
     39 # monkey-patch reST parser to disable alphabetic and roman enumerated lists
     40 from docutils.parsers.rst.states import Body
     41 Body.enum.converters['loweralpha'] = \
     42     Body.enum.converters['upperalpha'] = \
     43     Body.enum.converters['lowerroman'] = \
     44     Body.enum.converters['upperroman'] = lambda x: None
     45 
     46 # monkey-patch HTML and LaTeX translators to keep doctest blocks in the
     47 # doctest docs themselves
     48 orig_visit_literal_block = HTMLTranslator.visit_literal_block
     49 orig_depart_literal_block = LaTeXTranslator.depart_literal_block
     50 
     51 
     52 def new_visit_literal_block(self, node):
     53     meta = self.builder.env.metadata[self.builder.current_docname]
     54     old_trim_doctest_flags = self.highlighter.trim_doctest_flags
     55     if 'keepdoctest' in meta:
     56         self.highlighter.trim_doctest_flags = False
     57     try:
     58         orig_visit_literal_block(self, node)
     59     finally:
     60         self.highlighter.trim_doctest_flags = old_trim_doctest_flags
     61 
     62 
     63 def new_depart_literal_block(self, node):
     64     meta = self.builder.env.metadata[self.curfilestack[-1]]
     65     old_trim_doctest_flags = self.highlighter.trim_doctest_flags
     66     if 'keepdoctest' in meta:
     67         self.highlighter.trim_doctest_flags = False
     68     try:
     69         orig_depart_literal_block(self, node)
     70     finally:
     71         self.highlighter.trim_doctest_flags = old_trim_doctest_flags
     72 
     73 
     74 HTMLTranslator.visit_literal_block = new_visit_literal_block
     75 LaTeXTranslator.depart_literal_block = new_depart_literal_block
     76 
     77 
     78 # Support for marking up and linking to bugs.python.org issues
     79 
     80 def issue_role(typ, rawtext, text, lineno, inliner, options={}, content=[]):
     81     issue = utils.unescape(text)
     82     text = 'bpo-' + issue
     83     refnode = nodes.reference(text, text, refuri=ISSUE_URI % issue)
     84     return [refnode], []
     85 
     86 
     87 # Support for linking to Python source files easily
     88 
     89 def source_role(typ, rawtext, text, lineno, inliner, options={}, content=[]):
     90     has_t, title, target = split_explicit_title(text)
     91     title = utils.unescape(title)
     92     target = utils.unescape(target)
     93     refnode = nodes.reference(title, title, refuri=SOURCE_URI % target)
     94     return [refnode], []
     95 
     96 
     97 # Support for marking up implementation details
     98 
     99 class ImplementationDetail(Directive):
    100 
    101     has_content = True
    102     required_arguments = 0
    103     optional_arguments = 1
    104     final_argument_whitespace = True
    105 
    106     def run(self):
    107         pnode = nodes.compound(classes=['impl-detail'])
    108         content = self.content
    109         add_text = nodes.strong('CPython implementation detail:',
    110                                 'CPython implementation detail:')
    111         if self.arguments:
    112             n, m = self.state.inline_text(self.arguments[0], self.lineno)
    113             pnode.append(nodes.paragraph('', '', *(n + m)))
    114         self.state.nested_parse(content, self.content_offset, pnode)
    115         if pnode.children and isinstance(pnode[0], nodes.paragraph):
    116             pnode[0].insert(0, add_text)
    117             pnode[0].insert(1, nodes.Text(' '))
    118         else:
    119             pnode.insert(0, nodes.paragraph('', '', add_text))
    120         return [pnode]
    121 
    122 
    123 # Support for documenting decorators
    124 
    125 class PyDecoratorMixin(object):
    126     def handle_signature(self, sig, signode):
    127         ret = super(PyDecoratorMixin, self).handle_signature(sig, signode)
    128         signode.insert(0, addnodes.desc_addname('@', '@'))
    129         return ret
    130 
    131     def needs_arglist(self):
    132         return False
    133 
    134 
    135 class PyDecoratorFunction(PyDecoratorMixin, PyModulelevel):
    136     def run(self):
    137         # a decorator function is a function after all
    138         self.name = 'py:function'
    139         return PyModulelevel.run(self)
    140 
    141 
    142 class PyDecoratorMethod(PyDecoratorMixin, PyClassmember):
    143     def run(self):
    144         self.name = 'py:method'
    145         return PyClassmember.run(self)
    146 
    147 
    148 class PyCoroutineMixin(object):
    149     def handle_signature(self, sig, signode):
    150         ret = super(PyCoroutineMixin, self).handle_signature(sig, signode)
    151         signode.insert(0, addnodes.desc_annotation('coroutine ', 'coroutine '))
    152         return ret
    153 
    154 
    155 class PyCoroutineFunction(PyCoroutineMixin, PyModulelevel):
    156     def run(self):
    157         self.name = 'py:function'
    158         return PyModulelevel.run(self)
    159 
    160 
    161 class PyCoroutineMethod(PyCoroutineMixin, PyClassmember):
    162     def run(self):
    163         self.name = 'py:method'
    164         return PyClassmember.run(self)
    165 
    166 
    167 class PyAbstractMethod(PyClassmember):
    168 
    169     def handle_signature(self, sig, signode):
    170         ret = super(PyAbstractMethod, self).handle_signature(sig, signode)
    171         signode.insert(0, addnodes.desc_annotation('abstractmethod ',
    172                                                    'abstractmethod '))
    173         return ret
    174 
    175     def run(self):
    176         self.name = 'py:method'
    177         return PyClassmember.run(self)
    178 
    179 
    180 # Support for documenting version of removal in deprecations
    181 
    182 class DeprecatedRemoved(Directive):
    183     has_content = True
    184     required_arguments = 2
    185     optional_arguments = 1
    186     final_argument_whitespace = True
    187     option_spec = {}
    188 
    189     _label = 'Deprecated since version %s, will be removed in version %s'
    190 
    191     def run(self):
    192         node = addnodes.versionmodified()
    193         node.document = self.state.document
    194         node['type'] = 'deprecated-removed'
    195         version = (self.arguments[0], self.arguments[1])
    196         node['version'] = version
    197         text = self._label % version
    198         if len(self.arguments) == 3:
    199             inodes, messages = self.state.inline_text(self.arguments[2],
    200                                                       self.lineno+1)
    201             para = nodes.paragraph(self.arguments[2], '', *inodes)
    202             node.append(para)
    203         else:
    204             messages = []
    205         if self.content:
    206             self.state.nested_parse(self.content, self.content_offset, node)
    207         if len(node):
    208             if isinstance(node[0], nodes.paragraph) and node[0].rawsource:
    209                 content = nodes.inline(node[0].rawsource, translatable=True)
    210                 content.source = node[0].source
    211                 content.line = node[0].line
    212                 content += node[0].children
    213                 node[0].replace_self(nodes.paragraph('', '', content))
    214             node[0].insert(0, nodes.inline('', '%s: ' % text,
    215                                            classes=['versionmodified']))
    216         else:
    217             para = nodes.paragraph('', '',
    218                                    nodes.inline('', '%s.' % text,
    219                                                 classes=['versionmodified']))
    220             node.append(para)
    221         env = self.state.document.settings.env
    222         env.note_versionchange('deprecated', version[0], node, self.lineno)
    223         return [node] + messages
    224 
    225 
    226 # Support for including Misc/NEWS
    227 
    228 issue_re = re.compile('(?:[Ii]ssue #|bpo-)([0-9]+)')
    229 whatsnew_re = re.compile(r"(?im)^what's new in (.*?)\??$")
    230 
    231 
    232 class MiscNews(Directive):
    233     has_content = False
    234     required_arguments = 1
    235     optional_arguments = 0
    236     final_argument_whitespace = False
    237     option_spec = {}
    238 
    239     def run(self):
    240         fname = self.arguments[0]
    241         source = self.state_machine.input_lines.source(
    242             self.lineno - self.state_machine.input_offset - 1)
    243         source_dir = path.dirname(path.abspath(source))
    244         fpath = path.join(source_dir, fname)
    245         self.state.document.settings.record_dependencies.add(fpath)
    246         try:
    247             fp = codecs.open(fpath, encoding='utf-8')
    248             try:
    249                 content = fp.read()
    250             finally:
    251                 fp.close()
    252         except Exception:
    253             text = 'The NEWS file is not available.'
    254             node = nodes.strong(text, text)
    255             return [node]
    256         content = issue_re.sub(r'`bpo-\1 <https://bugs.python.org/issue\1>`__',
    257                                content)
    258         content = whatsnew_re.sub(r'\1', content)
    259         # remove first 3 lines as they are the main heading
    260         lines = ['.. default-role:: obj', ''] + content.splitlines()[3:]
    261         self.state_machine.insert_input(lines, fname)
    262         return []
    263 
    264 
    265 # Support for building "topic help" for pydoc
    266 
    267 pydoc_topic_labels = [
    268     'assert', 'assignment', 'atom-identifiers', 'atom-literals',
    269     'attribute-access', 'attribute-references', 'augassign', 'binary',
    270     'bitwise', 'bltin-code-objects', 'bltin-ellipsis-object',
    271     'bltin-null-object', 'bltin-type-objects', 'booleans',
    272     'break', 'callable-types', 'calls', 'class', 'comparisons', 'compound',
    273     'context-managers', 'continue', 'conversions', 'customization', 'debugger',
    274     'del', 'dict', 'dynamic-features', 'else', 'exceptions', 'execmodel',
    275     'exprlists', 'floating', 'for', 'formatstrings', 'function', 'global',
    276     'id-classes', 'identifiers', 'if', 'imaginary', 'import', 'in', 'integers',
    277     'lambda', 'lists', 'naming', 'nonlocal', 'numbers', 'numeric-types',
    278     'objects', 'operator-summary', 'pass', 'power', 'raise', 'return',
    279     'sequence-types', 'shifting', 'slicings', 'specialattrs', 'specialnames',
    280     'string-methods', 'strings', 'subscriptions', 'truth', 'try', 'types',
    281     'typesfunctions', 'typesmapping', 'typesmethods', 'typesmodules',
    282     'typesseq', 'typesseq-mutable', 'unary', 'while', 'with', 'yield'
    283 ]
    284 
    285 
    286 class PydocTopicsBuilder(Builder):
    287     name = 'pydoc-topics'
    288 
    289     def init(self):
    290         self.topics = {}
    291 
    292     def get_outdated_docs(self):
    293         return 'all pydoc topics'
    294 
    295     def get_target_uri(self, docname, typ=None):
    296         return ''  # no URIs
    297 
    298     def write(self, *ignored):
    299         writer = TextWriter(self)
    300         for label in self.status_iterator(pydoc_topic_labels,
    301                                           'building topics... ',
    302                                           length=len(pydoc_topic_labels)):
    303             if label not in self.env.domaindata['std']['labels']:
    304                 self.warn('label %r not in documentation' % label)
    305                 continue
    306             docname, labelid, sectname = self.env.domaindata['std']['labels'][label]
    307             doctree = self.env.get_and_resolve_doctree(docname, self)
    308             document = new_document('<section node>')
    309             document.append(doctree.ids[labelid])
    310             destination = StringOutput(encoding='utf-8')
    311             writer.write(document, destination)
    312             self.topics[label] = writer.output
    313 
    314     def finish(self):
    315         f = open(path.join(self.outdir, 'topics.py'), 'wb')
    316         try:
    317             f.write('# -*- coding: utf-8 -*-\n'.encode('utf-8'))
    318             f.write(('# Autogenerated by Sphinx on %s\n' % asctime()).encode('utf-8'))
    319             f.write(('topics = ' + pformat(self.topics) + '\n').encode('utf-8'))
    320         finally:
    321             f.close()
    322 
    323 
    324 # Support for documenting Opcodes
    325 
    326 opcode_sig_re = re.compile(r'(\w+(?:\+\d)?)(?:\s*\((.*)\))?')
    327 
    328 
    329 def parse_opcode_signature(env, sig, signode):
    330     """Transform an opcode signature into RST nodes."""
    331     m = opcode_sig_re.match(sig)
    332     if m is None:
    333         raise ValueError
    334     opname, arglist = m.groups()
    335     signode += addnodes.desc_name(opname, opname)
    336     if arglist is not None:
    337         paramlist = addnodes.desc_parameterlist()
    338         signode += paramlist
    339         paramlist += addnodes.desc_parameter(arglist, arglist)
    340     return opname.strip()
    341 
    342 
    343 # Support for documenting pdb commands
    344 
    345 pdbcmd_sig_re = re.compile(r'([a-z()!]+)\s*(.*)')
    346 
    347 # later...
    348 # pdbargs_tokens_re = re.compile(r'''[a-zA-Z]+  |  # identifiers
    349 #                                   [.,:]+     |  # punctuation
    350 #                                   [\[\]()]   |  # parens
    351 #                                   \s+           # whitespace
    352 #                                   ''', re.X)
    353 
    354 
    355 def parse_pdb_command(env, sig, signode):
    356     """Transform a pdb command signature into RST nodes."""
    357     m = pdbcmd_sig_re.match(sig)
    358     if m is None:
    359         raise ValueError
    360     name, args = m.groups()
    361     fullname = name.replace('(', '').replace(')', '')
    362     signode += addnodes.desc_name(name, name)
    363     if args:
    364         signode += addnodes.desc_addname(' '+args, ' '+args)
    365     return fullname
    366 
    367 
    368 def setup(app):
    369     app.add_role('issue', issue_role)
    370     app.add_role('source', source_role)
    371     app.add_directive('impl-detail', ImplementationDetail)
    372     app.add_directive('deprecated-removed', DeprecatedRemoved)
    373     app.add_builder(PydocTopicsBuilder)
    374     app.add_builder(suspicious.CheckSuspiciousMarkupBuilder)
    375     app.add_description_unit('opcode', 'opcode', '%s (opcode)',
    376                              parse_opcode_signature)
    377     app.add_description_unit('pdbcommand', 'pdbcmd', '%s (pdb command)',
    378                              parse_pdb_command)
    379     app.add_description_unit('2to3fixer', '2to3fixer', '%s (2to3 fixer)')
    380     app.add_directive_to_domain('py', 'decorator', PyDecoratorFunction)
    381     app.add_directive_to_domain('py', 'decoratormethod', PyDecoratorMethod)
    382     app.add_directive_to_domain('py', 'coroutinefunction', PyCoroutineFunction)
    383     app.add_directive_to_domain('py', 'coroutinemethod', PyCoroutineMethod)
    384     app.add_directive_to_domain('py', 'abstractmethod', PyAbstractMethod)
    385     app.add_directive('miscnews', MiscNews)
    386     return {'version': '1.0', 'parallel_read_safe': True}
    387