(root)/
Python-3.11.7/
Doc/
tools/
extensions/
pyspecific.py
       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}