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 io 14from os import getenv, path 15from time import asctime 16from pprint import pformat 17from docutils.io import StringOutput 18from docutils.parsers.rst import Directive 19from docutils.utils import new_document 20 21from docutils import nodes, utils 22 23from sphinx import addnodes 24from sphinx.builders import Builder 25try: 26 from sphinx.errors import NoUri 27except ImportError: 28 from sphinx.environment import NoUri 29from sphinx.locale import _ as sphinx_gettext 30from sphinx.util import status_iterator, logging 31from sphinx.util.docutils import SphinxDirective 32from sphinx.util.nodes import split_explicit_title 33from sphinx.writers.text import TextWriter, TextTranslator 34from sphinx.writers.latex import LaTeXTranslator 35 36try: 37 from sphinx.domains.python import PyFunction, PyMethod 38except ImportError: 39 from sphinx.domains.python import PyClassmember as PyMethod 40 from sphinx.domains.python import PyModulelevel as PyFunction 41 42# Support for checking for suspicious markup 43 44import suspicious 45 46 47ISSUE_URI = 'https://bugs.python.org/issue?@action=redirect&bpo=%s' 48GH_ISSUE_URI = 'https://github.com/python/cpython/issues/%s' 49SOURCE_URI = 'https://github.com/python/cpython/tree/3.11/%s' 50 51# monkey-patch reST parser to disable alphabetic and roman enumerated lists 52from docutils.parsers.rst.states import Body 53Body.enum.converters['loweralpha'] = \ 54 Body.enum.converters['upperalpha'] = \ 55 Body.enum.converters['lowerroman'] = \ 56 Body.enum.converters['upperroman'] = lambda x: None 57 58 59# Support for marking up and linking to bugs.python.org issues 60 61def issue_role(typ, rawtext, text, lineno, inliner, options={}, content=[]): 62 issue = utils.unescape(text) 63 # sanity check: there are no bpo issues within these two values 64 if 47261 < int(issue) < 400000: 65 msg = inliner.reporter.error(f'The BPO ID {text!r} seems too high -- ' 66 'use :gh:`...` for GitHub IDs', line=lineno) 67 prb = inliner.problematic(rawtext, rawtext, msg) 68 return [prb], [msg] 69 text = 'bpo-' + issue 70 refnode = nodes.reference(text, text, refuri=ISSUE_URI % issue) 71 return [refnode], [] 72 73 74# Support for marking up and linking to GitHub issues 75 76def gh_issue_role(typ, rawtext, text, lineno, inliner, options={}, content=[]): 77 issue = utils.unescape(text) 78 # sanity check: all GitHub issues have ID >= 32426 79 # even though some of them are also valid BPO IDs 80 if int(issue) < 32426: 81 msg = inliner.reporter.error(f'The GitHub ID {text!r} seems too low -- ' 82 'use :issue:`...` for BPO IDs', line=lineno) 83 prb = inliner.problematic(rawtext, rawtext, msg) 84 return [prb], [msg] 85 text = 'gh-' + issue 86 refnode = nodes.reference(text, text, refuri=GH_ISSUE_URI % issue) 87 return [refnode], [] 88 89 90# Support for linking to Python source files easily 91 92def source_role(typ, rawtext, text, lineno, inliner, options={}, content=[]): 93 has_t, title, target = split_explicit_title(text) 94 title = utils.unescape(title) 95 target = utils.unescape(target) 96 refnode = nodes.reference(title, title, refuri=SOURCE_URI % target) 97 return [refnode], [] 98 99 100# Support for marking up implementation details 101 102class ImplementationDetail(Directive): 103 104 has_content = True 105 final_argument_whitespace = True 106 107 # This text is copied to templates/dummy.html 108 label_text = 'CPython implementation detail:' 109 110 def run(self): 111 self.assert_has_content() 112 pnode = nodes.compound(classes=['impl-detail']) 113 label = sphinx_gettext(self.label_text) 114 content = self.content 115 add_text = nodes.strong(label, label) 116 self.state.nested_parse(content, self.content_offset, pnode) 117 content = nodes.inline(pnode[0].rawsource, translatable=True) 118 content.source = pnode[0].source 119 content.line = pnode[0].line 120 content += pnode[0].children 121 pnode[0].replace_self(nodes.paragraph( 122 '', '', add_text, nodes.Text(' '), content, translatable=False)) 123 return [pnode] 124 125 126# Support for documenting platform availability 127 128class Availability(SphinxDirective): 129 130 has_content = True 131 required_arguments = 1 132 optional_arguments = 0 133 final_argument_whitespace = True 134 135 # known platform, libc, and threading implementations 136 known_platforms = frozenset({ 137 "AIX", "Android", "BSD", "DragonFlyBSD", "Emscripten", "FreeBSD", 138 "Linux", "NetBSD", "OpenBSD", "POSIX", "Solaris", "Unix", "VxWorks", 139 "WASI", "Windows", "macOS", 140 # libc 141 "BSD libc", "glibc", "musl", 142 # POSIX platforms with pthreads 143 "pthreads", 144 }) 145 146 def run(self): 147 availability_ref = ':ref:`Availability <availability>`: ' 148 avail_nodes, avail_msgs = self.state.inline_text( 149 availability_ref + self.arguments[0], 150 self.lineno) 151 pnode = nodes.paragraph(availability_ref + self.arguments[0], 152 '', *avail_nodes, *avail_msgs) 153 self.set_source_info(pnode) 154 cnode = nodes.container("", pnode, classes=["availability"]) 155 self.set_source_info(cnode) 156 if self.content: 157 self.state.nested_parse(self.content, self.content_offset, cnode) 158 self.parse_platforms() 159 160 return [cnode] 161 162 def parse_platforms(self): 163 """Parse platform information from arguments 164 165 Arguments is a comma-separated string of platforms. A platform may 166 be prefixed with "not " to indicate that a feature is not available. 167 168 Example:: 169 170 .. availability:: Windows, Linux >= 4.2, not Emscripten, not WASI 171 172 Arguments like "Linux >= 3.17 with glibc >= 2.27" are currently not 173 parsed into separate tokens. 174 """ 175 platforms = {} 176 for arg in self.arguments[0].rstrip(".").split(","): 177 arg = arg.strip() 178 platform, _, version = arg.partition(" >= ") 179 if platform.startswith("not "): 180 version = False 181 platform = platform[4:] 182 elif not version: 183 version = True 184 platforms[platform] = version 185 186 unknown = set(platforms).difference(self.known_platforms) 187 if unknown: 188 cls = type(self) 189 logger = logging.getLogger(cls.__qualname__) 190 logger.warn( 191 f"Unknown platform(s) or syntax '{' '.join(sorted(unknown))}' " 192 f"in '.. availability:: {self.arguments[0]}', see " 193 f"{__file__}:{cls.__qualname__}.known_platforms for a set " 194 "known platforms." 195 ) 196 197 return platforms 198 199 200 201# Support for documenting audit event 202 203def audit_events_purge(app, env, docname): 204 """This is to remove from env.all_audit_events old traces of removed 205 documents. 206 """ 207 if not hasattr(env, 'all_audit_events'): 208 return 209 fresh_all_audit_events = {} 210 for name, event in env.all_audit_events.items(): 211 event["source"] = [(d, t) for d, t in event["source"] if d != docname] 212 if event["source"]: 213 # Only keep audit_events that have at least one source. 214 fresh_all_audit_events[name] = event 215 env.all_audit_events = fresh_all_audit_events 216 217 218def audit_events_merge(app, env, docnames, other): 219 """In Sphinx parallel builds, this merges env.all_audit_events from 220 subprocesses. 221 222 all_audit_events is a dict of names, with values like: 223 {'source': [(docname, target), ...], 'args': args} 224 """ 225 if not hasattr(other, 'all_audit_events'): 226 return 227 if not hasattr(env, 'all_audit_events'): 228 env.all_audit_events = {} 229 for name, value in other.all_audit_events.items(): 230 if name in env.all_audit_events: 231 env.all_audit_events[name]["source"].extend(value["source"]) 232 else: 233 env.all_audit_events[name] = value 234 235 236class AuditEvent(Directive): 237 238 has_content = True 239 required_arguments = 1 240 optional_arguments = 2 241 final_argument_whitespace = True 242 243 _label = [ 244 "Raises an :ref:`auditing event <auditing>` {name} with no arguments.", 245 "Raises an :ref:`auditing event <auditing>` {name} with argument {args}.", 246 "Raises an :ref:`auditing event <auditing>` {name} with arguments {args}.", 247 ] 248 249 @property 250 def logger(self): 251 cls = type(self) 252 return logging.getLogger(cls.__module__ + "." + cls.__name__) 253 254 def run(self): 255 name = self.arguments[0] 256 if len(self.arguments) >= 2 and self.arguments[1]: 257 args = (a.strip() for a in self.arguments[1].strip("'\"").split(",")) 258 args = [a for a in args if a] 259 else: 260 args = [] 261 262 label = sphinx_gettext(self._label[min(2, len(args))]) 263 text = label.format(name="``{}``".format(name), 264 args=", ".join("``{}``".format(a) for a in args if a)) 265 266 env = self.state.document.settings.env 267 if not hasattr(env, 'all_audit_events'): 268 env.all_audit_events = {} 269 270 new_info = { 271 'source': [], 272 'args': args 273 } 274 info = env.all_audit_events.setdefault(name, new_info) 275 if info is not new_info: 276 if not self._do_args_match(info['args'], new_info['args']): 277 self.logger.warn( 278 "Mismatched arguments for audit-event {}: {!r} != {!r}" 279 .format(name, info['args'], new_info['args']) 280 ) 281 282 ids = [] 283 try: 284 target = self.arguments[2].strip("\"'") 285 except (IndexError, TypeError): 286 target = None 287 if not target: 288 target = "audit_event_{}_{}".format( 289 re.sub(r'\W', '_', name), 290 len(info['source']), 291 ) 292 ids.append(target) 293 294 info['source'].append((env.docname, target)) 295 296 pnode = nodes.paragraph(text, classes=["audit-hook"], ids=ids) 297 pnode.line = self.lineno 298 if self.content: 299 self.state.nested_parse(self.content, self.content_offset, pnode) 300 else: 301 n, m = self.state.inline_text(text, self.lineno) 302 pnode.extend(n + m) 303 304 return [pnode] 305 306 # This list of sets are allowable synonyms for event argument names. 307 # If two names are in the same set, they are treated as equal for the 308 # purposes of warning. This won't help if number of arguments is 309 # different! 310 _SYNONYMS = [ 311 {"file", "path", "fd"}, 312 ] 313 314 def _do_args_match(self, args1, args2): 315 if args1 == args2: 316 return True 317 if len(args1) != len(args2): 318 return False 319 for a1, a2 in zip(args1, args2): 320 if a1 == a2: 321 continue 322 if any(a1 in s and a2 in s for s in self._SYNONYMS): 323 continue 324 return False 325 return True 326 327 328class audit_event_list(nodes.General, nodes.Element): 329 pass 330 331 332class AuditEventListDirective(Directive): 333 334 def run(self): 335 return [audit_event_list('')] 336 337 338# Support for documenting decorators 339 340class PyDecoratorMixin(object): 341 def handle_signature(self, sig, signode): 342 ret = super(PyDecoratorMixin, self).handle_signature(sig, signode) 343 signode.insert(0, addnodes.desc_addname('@', '@')) 344 return ret 345 346 def needs_arglist(self): 347 return False 348 349 350class PyDecoratorFunction(PyDecoratorMixin, PyFunction): 351 def run(self): 352 # a decorator function is a function after all 353 self.name = 'py:function' 354 return PyFunction.run(self) 355 356 357# TODO: Use sphinx.domains.python.PyDecoratorMethod when possible 358class PyDecoratorMethod(PyDecoratorMixin, PyMethod): 359 def run(self): 360 self.name = 'py:method' 361 return PyMethod.run(self) 362 363 364class PyCoroutineMixin(object): 365 def handle_signature(self, sig, signode): 366 ret = super(PyCoroutineMixin, self).handle_signature(sig, signode) 367 signode.insert(0, addnodes.desc_annotation('coroutine ', 'coroutine ')) 368 return ret 369 370 371class PyAwaitableMixin(object): 372 def handle_signature(self, sig, signode): 373 ret = super(PyAwaitableMixin, self).handle_signature(sig, signode) 374 signode.insert(0, addnodes.desc_annotation('awaitable ', 'awaitable ')) 375 return ret 376 377 378class PyCoroutineFunction(PyCoroutineMixin, PyFunction): 379 def run(self): 380 self.name = 'py:function' 381 return PyFunction.run(self) 382 383 384class PyCoroutineMethod(PyCoroutineMixin, PyMethod): 385 def run(self): 386 self.name = 'py:method' 387 return PyMethod.run(self) 388 389 390class PyAwaitableFunction(PyAwaitableMixin, PyFunction): 391 def run(self): 392 self.name = 'py:function' 393 return PyFunction.run(self) 394 395 396class PyAwaitableMethod(PyAwaitableMixin, PyMethod): 397 def run(self): 398 self.name = 'py:method' 399 return PyMethod.run(self) 400 401 402class PyAbstractMethod(PyMethod): 403 404 def handle_signature(self, sig, signode): 405 ret = super(PyAbstractMethod, self).handle_signature(sig, signode) 406 signode.insert(0, addnodes.desc_annotation('abstractmethod ', 407 'abstractmethod ')) 408 return ret 409 410 def run(self): 411 self.name = 'py:method' 412 return PyMethod.run(self) 413 414 415# Support for documenting version of removal in deprecations 416 417class DeprecatedRemoved(Directive): 418 has_content = True 419 required_arguments = 2 420 optional_arguments = 1 421 final_argument_whitespace = True 422 option_spec = {} 423 424 _deprecated_label = 'Deprecated since version {deprecated}, will be removed in version {removed}' 425 _removed_label = 'Deprecated since version {deprecated}, removed in version {removed}' 426 427 def run(self): 428 node = addnodes.versionmodified() 429 node.document = self.state.document 430 node['type'] = 'deprecated-removed' 431 version = (self.arguments[0], self.arguments[1]) 432 node['version'] = version 433 env = self.state.document.settings.env 434 current_version = tuple(int(e) for e in env.config.version.split('.')) 435 removed_version = tuple(int(e) for e in self.arguments[1].split('.')) 436 if current_version < removed_version: 437 label = self._deprecated_label 438 else: 439 label = self._removed_label 440 441 label = sphinx_gettext(label) 442 text = label.format(deprecated=self.arguments[0], removed=self.arguments[1]) 443 if len(self.arguments) == 3: 444 inodes, messages = self.state.inline_text(self.arguments[2], 445 self.lineno+1) 446 para = nodes.paragraph(self.arguments[2], '', *inodes, translatable=False) 447 node.append(para) 448 else: 449 messages = [] 450 if self.content: 451 self.state.nested_parse(self.content, self.content_offset, node) 452 if len(node): 453 if isinstance(node[0], nodes.paragraph) and node[0].rawsource: 454 content = nodes.inline(node[0].rawsource, translatable=True) 455 content.source = node[0].source 456 content.line = node[0].line 457 content += node[0].children 458 node[0].replace_self(nodes.paragraph('', '', content, translatable=False)) 459 node[0].insert(0, nodes.inline('', '%s: ' % text, 460 classes=['versionmodified'])) 461 else: 462 para = nodes.paragraph('', '', 463 nodes.inline('', '%s.' % text, 464 classes=['versionmodified']), 465 translatable=False) 466 node.append(para) 467 env = self.state.document.settings.env 468 env.get_domain('changeset').note_changeset(node) 469 return [node] + messages 470 471 472# Support for including Misc/NEWS 473 474issue_re = re.compile('(?:[Ii]ssue #|bpo-)([0-9]+)', re.I) 475gh_issue_re = re.compile('(?:gh-issue-|gh-)([0-9]+)', re.I) 476whatsnew_re = re.compile(r"(?im)^what's new in (.*?)\??$") 477 478 479class MiscNews(Directive): 480 has_content = False 481 required_arguments = 1 482 optional_arguments = 0 483 final_argument_whitespace = False 484 option_spec = {} 485 486 def run(self): 487 fname = self.arguments[0] 488 source = self.state_machine.input_lines.source( 489 self.lineno - self.state_machine.input_offset - 1) 490 source_dir = getenv('PY_MISC_NEWS_DIR') 491 if not source_dir: 492 source_dir = path.dirname(path.abspath(source)) 493 fpath = path.join(source_dir, fname) 494 self.state.document.settings.record_dependencies.add(fpath) 495 try: 496 with io.open(fpath, encoding='utf-8') as fp: 497 content = fp.read() 498 except Exception: 499 text = 'The NEWS file is not available.' 500 node = nodes.strong(text, text) 501 return [node] 502 content = issue_re.sub(r':issue:`\1`', content) 503 # Fallback handling for the GitHub issue 504 content = gh_issue_re.sub(r':gh:`\1`', content) 505 content = whatsnew_re.sub(r'\1', content) 506 # remove first 3 lines as they are the main heading 507 lines = ['.. default-role:: obj', ''] + content.splitlines()[3:] 508 self.state_machine.insert_input(lines, fname) 509 return [] 510 511 512# Support for building "topic help" for pydoc 513 514pydoc_topic_labels = [ 515 'assert', 'assignment', 'async', 'atom-identifiers', 'atom-literals', 516 'attribute-access', 'attribute-references', 'augassign', 'await', 517 'binary', 'bitwise', 'bltin-code-objects', 'bltin-ellipsis-object', 518 'bltin-null-object', 'bltin-type-objects', 'booleans', 519 'break', 'callable-types', 'calls', 'class', 'comparisons', 'compound', 520 'context-managers', 'continue', 'conversions', 'customization', 'debugger', 521 'del', 'dict', 'dynamic-features', 'else', 'exceptions', 'execmodel', 522 'exprlists', 'floating', 'for', 'formatstrings', 'function', 'global', 523 'id-classes', 'identifiers', 'if', 'imaginary', 'import', 'in', 'integers', 524 'lambda', 'lists', 'naming', 'nonlocal', 'numbers', 'numeric-types', 525 'objects', 'operator-summary', 'pass', 'power', 'raise', 'return', 526 'sequence-types', 'shifting', 'slicings', 'specialattrs', 'specialnames', 527 'string-methods', 'strings', 'subscriptions', 'truth', 'try', 'types', 528 'typesfunctions', 'typesmapping', 'typesmethods', 'typesmodules', 529 'typesseq', 'typesseq-mutable', 'unary', 'while', 'with', 'yield' 530] 531 532 533class PydocTopicsBuilder(Builder): 534 name = 'pydoc-topics' 535 536 default_translator_class = TextTranslator 537 538 def init(self): 539 self.topics = {} 540 self.secnumbers = {} 541 542 def get_outdated_docs(self): 543 return 'all pydoc topics' 544 545 def get_target_uri(self, docname, typ=None): 546 return '' # no URIs 547 548 def write(self, *ignored): 549 writer = TextWriter(self) 550 for label in status_iterator(pydoc_topic_labels, 551 'building topics... ', 552 length=len(pydoc_topic_labels)): 553 if label not in self.env.domaindata['std']['labels']: 554 self.env.logger.warn('label %r not in documentation' % label) 555 continue 556 docname, labelid, sectname = self.env.domaindata['std']['labels'][label] 557 doctree = self.env.get_and_resolve_doctree(docname, self) 558 document = new_document('<section node>') 559 document.append(doctree.ids[labelid]) 560 destination = StringOutput(encoding='utf-8') 561 writer.write(document, destination) 562 self.topics[label] = writer.output 563 564 def finish(self): 565 f = open(path.join(self.outdir, 'topics.py'), 'wb') 566 try: 567 f.write('# -*- coding: utf-8 -*-\n'.encode('utf-8')) 568 f.write(('# Autogenerated by Sphinx on %s\n' % asctime()).encode('utf-8')) 569 f.write(('topics = ' + pformat(self.topics) + '\n').encode('utf-8')) 570 finally: 571 f.close() 572 573 574# Support for documenting Opcodes 575 576opcode_sig_re = re.compile(r'(\w+(?:\+\d)?)(?:\s*\((.*)\))?') 577 578 579def parse_opcode_signature(env, sig, signode): 580 """Transform an opcode signature into RST nodes.""" 581 m = opcode_sig_re.match(sig) 582 if m is None: 583 raise ValueError 584 opname, arglist = m.groups() 585 signode += addnodes.desc_name(opname, opname) 586 if arglist is not None: 587 paramlist = addnodes.desc_parameterlist() 588 signode += paramlist 589 paramlist += addnodes.desc_parameter(arglist, arglist) 590 return opname.strip() 591 592 593# Support for documenting pdb commands 594 595pdbcmd_sig_re = re.compile(r'([a-z()!]+)\s*(.*)') 596 597# later... 598# pdbargs_tokens_re = re.compile(r'''[a-zA-Z]+ | # identifiers 599# [.,:]+ | # punctuation 600# [\[\]()] | # parens 601# \s+ # whitespace 602# ''', re.X) 603 604 605def parse_pdb_command(env, sig, signode): 606 """Transform a pdb command signature into RST nodes.""" 607 m = pdbcmd_sig_re.match(sig) 608 if m is None: 609 raise ValueError 610 name, args = m.groups() 611 fullname = name.replace('(', '').replace(')', '') 612 signode += addnodes.desc_name(name, name) 613 if args: 614 signode += addnodes.desc_addname(' '+args, ' '+args) 615 return fullname 616 617 618def process_audit_events(app, doctree, fromdocname): 619 for node in doctree.traverse(audit_event_list): 620 break 621 else: 622 return 623 624 env = app.builder.env 625 626 table = nodes.table(cols=3) 627 group = nodes.tgroup( 628 '', 629 nodes.colspec(colwidth=30), 630 nodes.colspec(colwidth=55), 631 nodes.colspec(colwidth=15), 632 cols=3, 633 ) 634 head = nodes.thead() 635 body = nodes.tbody() 636 637 table += group 638 group += head 639 group += body 640 641 row = nodes.row() 642 row += nodes.entry('', nodes.paragraph('', nodes.Text('Audit event'))) 643 row += nodes.entry('', nodes.paragraph('', nodes.Text('Arguments'))) 644 row += nodes.entry('', nodes.paragraph('', nodes.Text('References'))) 645 head += row 646 647 for name in sorted(getattr(env, "all_audit_events", ())): 648 audit_event = env.all_audit_events[name] 649 650 row = nodes.row() 651 node = nodes.paragraph('', nodes.Text(name)) 652 row += nodes.entry('', node) 653 654 node = nodes.paragraph() 655 for i, a in enumerate(audit_event['args']): 656 if i: 657 node += nodes.Text(", ") 658 node += nodes.literal(a, nodes.Text(a)) 659 row += nodes.entry('', node) 660 661 node = nodes.paragraph() 662 backlinks = enumerate(sorted(set(audit_event['source'])), start=1) 663 for i, (doc, label) in backlinks: 664 if isinstance(label, str): 665 ref = nodes.reference("", nodes.Text("[{}]".format(i)), internal=True) 666 try: 667 ref['refuri'] = "{}#{}".format( 668 app.builder.get_relative_uri(fromdocname, doc), 669 label, 670 ) 671 except NoUri: 672 continue 673 node += ref 674 row += nodes.entry('', node) 675 676 body += row 677 678 for node in doctree.traverse(audit_event_list): 679 node.replace_self(table) 680 681 682def patch_pairindextypes(app, _env) -> None: 683 """Remove all entries from ``pairindextypes`` before writing POT files. 684 685 We want to run this just before writing output files, as the check to 686 circumvent is in ``I18nBuilder.write_doc()``. 687 As such, we link this to ``env-check-consistency``, even though it has 688 nothing to do with the environment consistency check. 689 """ 690 if app.builder.name != 'gettext': 691 return 692 693 # allow translating deprecated index entries 694 try: 695 from sphinx.domains.python import pairindextypes 696 except ImportError: 697 pass 698 else: 699 # Sphinx checks if a 'pair' type entry on an index directive is one of 700 # the Sphinx-translated pairindextypes values. As we intend to move 701 # away from this, we need Sphinx to believe that these values don't 702 # exist, by deleting them when using the gettext builder. 703 pairindextypes.clear() 704 705 706def setup(app): 707 app.add_role('issue', issue_role) 708 app.add_role('gh', gh_issue_role) 709 app.add_role('source', source_role) 710 app.add_directive('impl-detail', ImplementationDetail) 711 app.add_directive('availability', Availability) 712 app.add_directive('audit-event', AuditEvent) 713 app.add_directive('audit-event-table', AuditEventListDirective) 714 app.add_directive('deprecated-removed', DeprecatedRemoved) 715 app.add_builder(PydocTopicsBuilder) 716 app.add_builder(suspicious.CheckSuspiciousMarkupBuilder) 717 app.add_object_type('opcode', 'opcode', '%s (opcode)', parse_opcode_signature) 718 app.add_object_type('pdbcommand', 'pdbcmd', '%s (pdb command)', parse_pdb_command) 719 app.add_object_type('2to3fixer', '2to3fixer', '%s (2to3 fixer)') 720 app.add_directive_to_domain('py', 'decorator', PyDecoratorFunction) 721 app.add_directive_to_domain('py', 'decoratormethod', PyDecoratorMethod) 722 app.add_directive_to_domain('py', 'coroutinefunction', PyCoroutineFunction) 723 app.add_directive_to_domain('py', 'coroutinemethod', PyCoroutineMethod) 724 app.add_directive_to_domain('py', 'awaitablefunction', PyAwaitableFunction) 725 app.add_directive_to_domain('py', 'awaitablemethod', PyAwaitableMethod) 726 app.add_directive_to_domain('py', 'abstractmethod', PyAbstractMethod) 727 app.add_directive('miscnews', MiscNews) 728 app.connect('env-check-consistency', patch_pairindextypes) 729 app.connect('doctree-resolved', process_audit_events) 730 app.connect('env-merge-info', audit_events_merge) 731 app.connect('env-purge-doc', audit_events_purge) 732 return {'version': '1.0', 'parallel_read_safe': True} 733