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