1  # -*- coding: utf-8 -*-
       2  """
       3      c_annotations.py
       4      ~~~~~~~~~~~~~~~~
       5  
       6      Supports annotations for C API elements:
       7  
       8      * reference count annotations for C API functions.  Based on
       9        refcount.py and anno-api.py in the old Python documentation tools.
      10  
      11      * stable API annotations
      12  
      13      Usage:
      14      * Set the `refcount_file` config value to the path to the reference
      15      count data file.
      16      * Set the `stable_abi_file` config value to the path to stable ABI list.
      17  
      18      :copyright: Copyright 2007-2014 by Georg Brandl.
      19      :license: Python license.
      20  """
      21  
      22  from os import path
      23  import docutils
      24  from docutils import nodes
      25  from docutils.parsers.rst import directives
      26  from docutils.parsers.rst import Directive
      27  from docutils.statemachine import StringList
      28  from sphinx.locale import _ as sphinx_gettext
      29  import csv
      30  
      31  from sphinx import addnodes
      32  from sphinx.domains.c import CObject
      33  
      34  
      35  REST_ROLE_MAP = {
      36      'function': 'func',
      37      'var': 'data',
      38      'type': 'type',
      39      'macro': 'macro',
      40      'type': 'type',
      41      'member': 'member',
      42  }
      43  
      44  
      45  # Monkeypatch nodes.Node.findall for forwards compatability
      46  # This patch can be dropped when the minimum Sphinx version is 4.4.0
      47  # or the minimum Docutils version is 0.18.1.
      48  if docutils.__version_info__ < (0, 18, 1):
      49      def findall(self, *args, **kwargs):
      50          return iter(self.traverse(*args, **kwargs))
      51  
      52      nodes.Node.findall = findall
      53  
      54  
      55  class ESC[4;38;5;81mRCEntry:
      56      def __init__(self, name):
      57          self.name = name
      58          self.args = []
      59          self.result_type = ''
      60          self.result_refs = None
      61  
      62  
      63  class ESC[4;38;5;81mAnnotations:
      64      def __init__(self, refcount_filename, stable_abi_file):
      65          self.refcount_data = {}
      66          with open(refcount_filename, 'r') as fp:
      67              for line in fp:
      68                  line = line.strip()
      69                  if line[:1] in ("", "#"):
      70                      # blank lines and comments
      71                      continue
      72                  parts = line.split(":", 4)
      73                  if len(parts) != 5:
      74                      raise ValueError("Wrong field count in %r" % line)
      75                  function, type, arg, refcount, comment = parts
      76                  # Get the entry, creating it if needed:
      77                  try:
      78                      entry = self.refcount_data[function]
      79                  except KeyError:
      80                      entry = self.refcount_data[function] = RCEntry(function)
      81                  if not refcount or refcount == "null":
      82                      refcount = None
      83                  else:
      84                      refcount = int(refcount)
      85                  # Update the entry with the new parameter or the result
      86                  # information.
      87                  if arg:
      88                      entry.args.append((arg, type, refcount))
      89                  else:
      90                      entry.result_type = type
      91                      entry.result_refs = refcount
      92  
      93          self.stable_abi_data = {}
      94          with open(stable_abi_file, 'r') as fp:
      95              for record in csv.DictReader(fp):
      96                  role = record['role']
      97                  name = record['name']
      98                  self.stable_abi_data[name] = record
      99  
     100      def add_annotations(self, app, doctree):
     101          for node in doctree.findall(addnodes.desc_content):
     102              par = node.parent
     103              if par['domain'] != 'c':
     104                  continue
     105              if not par[0].has_key('ids') or not par[0]['ids']:
     106                  continue
     107              name = par[0]['ids'][0]
     108              if name.startswith("c."):
     109                  name = name[2:]
     110  
     111              objtype = par['objtype']
     112  
     113              # Stable ABI annotation. These have two forms:
     114              #   Part of the [Stable ABI](link).
     115              #   Part of the [Stable ABI](link) since version X.Y.
     116              # For structs, there's some more info in the message:
     117              #   Part of the [Limited API](link) (as an opaque struct).
     118              #   Part of the [Stable ABI](link) (including all members).
     119              #   Part of the [Limited API](link) (Only some members are part
     120              #       of the stable ABI.).
     121              # ... all of which can have "since version X.Y" appended.
     122              record = self.stable_abi_data.get(name)
     123              if record:
     124                  if record['role'] != objtype:
     125                      raise ValueError(
     126                          f"Object type mismatch in limited API annotation "
     127                          f"for {name}: {record['role']!r} != {objtype!r}")
     128                  stable_added = record['added']
     129                  message = ' Part of the '
     130                  emph_node = nodes.emphasis(message, message,
     131                                             classes=['stableabi'])
     132                  ref_node = addnodes.pending_xref(
     133                      'Stable ABI', refdomain="std", reftarget='stable',
     134                      reftype='ref', refexplicit="False")
     135                  struct_abi_kind = record['struct_abi_kind']
     136                  if struct_abi_kind in {'opaque', 'members'}:
     137                      ref_node += nodes.Text('Limited API')
     138                  else:
     139                      ref_node += nodes.Text('Stable ABI')
     140                  emph_node += ref_node
     141                  if struct_abi_kind == 'opaque':
     142                      emph_node += nodes.Text(' (as an opaque struct)')
     143                  elif struct_abi_kind == 'full-abi':
     144                      emph_node += nodes.Text(' (including all members)')
     145                  if record['ifdef_note']:
     146                      emph_node += nodes.Text(' ' + record['ifdef_note'])
     147                  if stable_added == '3.2':
     148                      # Stable ABI was introduced in 3.2.
     149                      pass
     150                  else:
     151                      emph_node += nodes.Text(f' since version {stable_added}')
     152                  emph_node += nodes.Text('.')
     153                  if struct_abi_kind == 'members':
     154                      emph_node += nodes.Text(
     155                          ' (Only some members are part of the stable ABI.)')
     156                  node.insert(0, emph_node)
     157  
     158              # Unstable API annotation.
     159              if name.startswith('PyUnstable'):
     160                  warn_node = nodes.admonition(
     161                      classes=['unstable-c-api', 'warning'])
     162                  message = 'This is '
     163                  emph_node = nodes.emphasis(message, message)
     164                  ref_node = addnodes.pending_xref(
     165                      'Unstable API', refdomain="std",
     166                      reftarget='unstable-c-api',
     167                      reftype='ref', refexplicit="False")
     168                  ref_node += nodes.Text('Unstable API')
     169                  emph_node += ref_node
     170                  emph_node += nodes.Text('. It may change without warning in minor releases.')
     171                  warn_node += emph_node
     172                  node.insert(0, warn_node)
     173  
     174              # Return value annotation
     175              if objtype != 'function':
     176                  continue
     177              entry = self.refcount_data.get(name)
     178              if not entry:
     179                  continue
     180              elif not entry.result_type.endswith("Object*"):
     181                  continue
     182              if entry.result_refs is None:
     183                  rc = sphinx_gettext('Return value: Always NULL.')
     184              elif entry.result_refs:
     185                  rc = sphinx_gettext('Return value: New reference.')
     186              else:
     187                  rc = sphinx_gettext('Return value: Borrowed reference.')
     188              node.insert(0, nodes.emphasis(rc, rc, classes=['refcount']))
     189  
     190  
     191  def init_annotations(app):
     192      annotations = Annotations(
     193          path.join(app.srcdir, app.config.refcount_file),
     194          path.join(app.srcdir, app.config.stable_abi_file),
     195      )
     196      app.connect('doctree-read', annotations.add_annotations)
     197  
     198      class ESC[4;38;5;81mLimitedAPIList(ESC[4;38;5;149mDirective):
     199  
     200          has_content = False
     201          required_arguments = 0
     202          optional_arguments = 0
     203          final_argument_whitespace = True
     204  
     205          def run(self):
     206              content = []
     207              for record in annotations.stable_abi_data.values():
     208                  role = REST_ROLE_MAP[record['role']]
     209                  name = record['name']
     210                  content.append(f'* :c:{role}:`{name}`')
     211  
     212              pnode = nodes.paragraph()
     213              self.state.nested_parse(StringList(content), 0, pnode)
     214              return [pnode]
     215  
     216      app.add_directive('limited-api-list', LimitedAPIList)
     217  
     218  
     219  def setup(app):
     220      app.add_config_value('refcount_file', '', True)
     221      app.add_config_value('stable_abi_file', '', True)
     222      app.connect('builder-inited', init_annotations)
     223  
     224      # monkey-patch C object...
     225      CObject.option_spec = {
     226          'noindex': directives.flag,
     227          'stableabi': directives.flag,
     228      }
     229      old_handle_signature = CObject.handle_signature
     230      def new_handle_signature(self, sig, signode):
     231          signode.parent['stableabi'] = 'stableabi' in self.options
     232          return old_handle_signature(self, sig, signode)
     233      CObject.handle_signature = new_handle_signature
     234      return {'version': '1.0', 'parallel_read_safe': True}