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