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