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