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 12import re 13import codecs 14from os import path 15from time import asctime 16from pprint import pformat 17from docutils.io import StringOutput 18from docutils.utils import new_document 19 20from docutils import nodes, utils 21 22from sphinx import addnodes 23from sphinx.builders import Builder 24from sphinx.util.nodes import split_explicit_title 25from sphinx.util.compat import Directive 26from sphinx.writers.html import HTMLTranslator 27from sphinx.writers.text import TextWriter 28from sphinx.writers.latex import LaTeXTranslator 29from sphinx.domains.python import PyModulelevel, PyClassmember 30 31# Support for checking for suspicious markup 32 33import suspicious 34 35 36ISSUE_URI = 'https://bugs.python.org/issue%s' 37SOURCE_URI = 'https://github.com/python/cpython/tree/3.6/%s' 38 39# monkey-patch reST parser to disable alphabetic and roman enumerated lists 40from docutils.parsers.rst.states import Body 41Body.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 48orig_visit_literal_block = HTMLTranslator.visit_literal_block 49orig_depart_literal_block = LaTeXTranslator.depart_literal_block 50 51 52def 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 63def 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 74HTMLTranslator.visit_literal_block = new_visit_literal_block 75LaTeXTranslator.depart_literal_block = new_depart_literal_block 76 77 78# Support for marking up and linking to bugs.python.org issues 79 80def 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 89def 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 99class 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 125class 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 135class 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 142class PyDecoratorMethod(PyDecoratorMixin, PyClassmember): 143 def run(self): 144 self.name = 'py:method' 145 return PyClassmember.run(self) 146 147 148class 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 155class PyCoroutineFunction(PyCoroutineMixin, PyModulelevel): 156 def run(self): 157 self.name = 'py:function' 158 return PyModulelevel.run(self) 159 160 161class PyCoroutineMethod(PyCoroutineMixin, PyClassmember): 162 def run(self): 163 self.name = 'py:method' 164 return PyClassmember.run(self) 165 166 167class 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 182class 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 228issue_re = re.compile('(?:[Ii]ssue #|bpo-)([0-9]+)') 229whatsnew_re = re.compile(r"(?im)^what's new in (.*?)\??$") 230 231 232class 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 267pydoc_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 286class 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 326opcode_sig_re = re.compile(r'(\w+(?:\+\d)?)(?:\s*\((.*)\))?') 327 328 329def 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 345pdbcmd_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 355def 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 368def 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