epydoc.docwriter.xlink

Package epydoc :: Package docwriter :: Module xlink |
[hide private]
[frames] | no frames]

# Source Code for Module epydoc.docwriter.xlink

      1  """
      2  A Docutils_ interpreted text role for cross-API reference support.
      3
      4  This module allows a Docutils_ document to refer to elements defined in
      5  external API documentation. It is possible to refer to many external API
      6  from the same document.
      7
      8  Each API documentation is assigned a new interpreted text role: using such
      9  interpreted text, an user can specify an object name inside an API
     10  documentation. The system will convert such text into an url and generate a
     11  reference to it. For example, if the API ``db`` is defined, being a database
     12  package, then a certain method may be referred as::
     13
     14      :db:`Connection.cursor()`
     15
     16  To define a new API, an *index file* must be provided. This file contains
     17  a mapping from the object name to the URL part required to resolve such object.
     18
     19  Index file
     20  ----------
     21
     22  Each line in the the index file describes an object.
     23
     24  Each line contains the fully qualified name of the object and the URL at which
     25  the documentation is located. The fields are separated by a ``<tab>``
     26  character.
     27
     28  The URL's in the file are relative from the documentation root: the system can
     29  be configured to add a prefix in front of each returned URL.
     30
     31  Allowed names
     32  -------------
     33
     34  When a name is used in an API text role, it is split over any *separator*.
     35  The separators defined are '``.``', '``::``', '``->``'. All the text from the
     36  first noise char (neither a separator nor alphanumeric or '``_``') is
     37  discarded. The same algorithm is applied when the index file is read.
     38
     39  First the sequence of name parts is looked for in the provided index file.
     40  If no matching name is found, a partial match against the trailing part of the
     41  names in the index is performed. If no object is found, or if the trailing part
     42  of the name may refer to many objects, a warning is issued and no reference
     43  is created.
     44
     45  Configuration
     46  -------------
     47
     48  This module provides the class `ApiLinkReader` a replacement for the Docutils
     49  standalone reader. Such reader specifies the settings required for the
     50  API canonical roles configuration. The same command line options are exposed by
     51  Epydoc.
     52
     53  The script ``apirst2html.py`` is a frontend for the `ApiLinkReader` reader.
     54
     55  API Linking Options::
     56
     57      --external-api=NAME
     58                          Define a new API document.  A new interpreted text
     59                          role NAME will be added.
     60      --external-api-file=NAME:FILENAME
     61                          Use records in FILENAME to resolve objects in the API
     62                          named NAME.
     63      --external-api-root=NAME:STRING
     64                          Use STRING as prefix for the URL generated from the
     65                          API NAME.
     66
     67  .. _Docutils: http://docutils.sourceforge.net/
     68  """
     69
     70  # $Id: xlink.py 1586 2007-03-14 01:53:42Z dvarrazzo $
     71  __version__ = "$Revision: 1586 $"[11:-2]
     72  __author__ = "Daniele Varrazzo"
     73  __copyright__ = "Copyright (C) 2007 by Daniele Varrazzo"
     74  __docformat__ = 'reStructuredText en'
     75
     76  import re
     77  import sys
     78  from optparse import OptionValueError
     79
     80  from epydoc import log
     81
     82 -class UrlGenerator:
     83      """
     84      Generate URL from an object name.
     85      """
     86 -    class IndexAmbiguous(IndexError):
     87          """
     88          The name looked for is ambiguous
     89          """
     90
     91 -    def get_url(self, name):
     92          """Look for a name and return the matching URL documentation.
     93
     94          First look for a fully qualified name. If not found, try with partial
     95          name.
     96
     97          If no url exists for the given object, return `None`.
     98
     99          :Parameters:
    100            `name` : `str`
    101              the name to look for
    102
    103          :return: the URL that can be used to reach the `name` documentation.
    104              `None` if no such URL exists.
    105          :rtype: `str`
    106
    107          :Exceptions:
    108            - `IndexError`: no object found with `name`
    109            - `DocUrlGenerator.IndexAmbiguous` : more than one object found with
    110              a non-fully qualified name; notice that this is an ``IndexError``
    111              subclass
    112          """
    113          raise NotImplementedError
    114
    115 -    def get_canonical_name(self, name):
    116          """
    117          Convert an object name into a canonical name.
    118
    119          the canonical name of an object is a tuple of strings containing its
    120          name fragments, splitted on any allowed separator ('``.``', '``::``',
    121          '``->``').
    122
    123          Noise such parenthesis to indicate a function is discarded.
    124
    125          :Parameters:
    126            `name` : `str`
    127              an object name, such as ``os.path.prefix()`` or ``lib::foo::bar``
    128
    129          :return: the fully qualified name such ``('os', 'path', 'prefix')`` and
    130              ``('lib', 'foo', 'bar')``
    131          :rtype: `tuple` of `str`
    132          """
    133          rv = []
    134          for m in self._SEP_RE.finditer(name):
    135              groups = m.groups()
    136              if groups[0] is not None:
    137                  rv.append(groups[0])
    138              elif groups[2] is not None:
    139                  break
    140
    141          return tuple(rv)
    142
    143      _SEP_RE = re.compile(r"""(?x)
    144          # Tokenize the input into keyword, separator, noise
    145          ([a-zA-Z0-9_]+)         |   # A keyword is a alphanum word
    146          ( \. | \:\: | \-\> )    |   # These are the allowed separators
    147          (.)                         # If it doesn't fit, it's noise.
    148              # Matching a single noise char is enough, because it
    149              # is used to break the tokenization as soon as some noise
    150              # is found.
    151          """)
    152
    153
    154 -class VoidUrlGenerator(UrlGenerator):
    155      """
    156      Don't actually know any url, but don't report any error.
    157
    158      Useful if an index file is not available, but a document linking to it
    159      is to be generated, and warnings are to be avoided.
    160
    161      Don't report any object as missing, Don't return any url anyway.
    162      """
    163 -    def get_url(self, name):
    164          return None
    165
    166
    167 -class DocUrlGenerator(UrlGenerator):
    168      """
    169      Read a *documentation index* and generate URL's for it.
    170      """
    171 -    def __init__(self):
    172          self._exact_matches = {}
    173          """
    174          A map from an object fully qualified name to its URL.
    175
    176          Values are both the name as tuple of fragments and as read from the
    177          records (see `load_records()`), mostly to help `_partial_names` to
    178          perform lookup for unambiguous names.
    179          """
    180
    181          self._partial_names= {}
    182          """
    183          A map from partial names to the fully qualified names they may refer.
    184
    185          The keys are the possible left sub-tuples of fully qualified names,
    186          the values are list of strings as provided by the index.
    187
    188          If the list for a given tuple contains a single item, the partial
    189          match is not ambuguous. In this case the string can be looked up in
    190          `_exact_matches`.
    191
    192          If the name fragment is ambiguous, a warning may be issued to the user.
    193          The items can be used to provide an informative message to the user,
    194          to help him qualifying the name in a unambiguous manner.
    195          """
    196
    197          self.prefix = ''
    198          """
    199          Prefix portion for the URL's returned by `get_url()`.
    200          """
    201
    202          self._filename = None
    203          """
    204          Not very important: only for logging.
    205          """
    206
    207 -    def get_url(self, name):
    208          cname = self.get_canonical_name(name)
    209          url = self._exact_matches.get(cname, None)
    210          if url is None:
    211
    212              # go for a partial match
    213              vals = self._partial_names.get(cname)
    214              if vals is None:
    215                  raise IndexError(
    216                      "no object named '%s' found" % (name))
    217
    218              elif len(vals) == 1:
    219                  url = self._exact_matches[vals[0]]
    220
    221              else:
    222                  raise self.IndexAmbiguous(
    223                      "found %d objects that '%s' may refer to: %s"
    224                      % (len(vals), name, ", ".join(["'%s'" % n for n in vals])))
    225
    226          return self.prefix + url
    227
    228      #{ Content loading
    229      #  ---------------
    230
    231 -    def clear(self):
    232          """
    233          Clear the current class content.
    234          """
    235          self._exact_matches.clear()
    236          self._partial_names.clear()
    237
    238 -    def load_index(self, f):
    239          """
    240          Read the content of an index file.
    241
    242          Populate the internal maps with the file content using `load_records()`.
    243
    244          :Parameters:
    245            f : `str` or file
    246              a file name or file-like object fron which read the index.
    247          """
    248          self._filename = str(f)
    249
    250          if isinstance(f, basestring):
    251              f = open(f)
    252
    253          self.load_records(self._iter_tuples(f))
    254
    255 -    def _iter_tuples(self, f):
    256          """Iterate on a file returning 2-tuples."""
    257          for nrow, row in enumerate(f):
    258              # skip blank lines
    259              row = row.rstrip()
    260              if not row: continue
    261
    262              rec = row.split('\t', 2)
    263              if len(rec) == 2:
    264                  yield rec
    265              else:
    266                  log.warning("invalid row in '%s' row %d: '%s'"
    267                              % (self._filename, nrow+1, row))
    268
    269 -    def load_records(self, records):
    270          """
    271          Read a sequence of pairs name -> url and populate the internal maps.
    272
    273          :Parameters:
    274            records : iterable
    275              the sequence of pairs (*name*, *url*) to add to the maps.
    276          """
    277          for name, url in records:
    278              cname = self.get_canonical_name(name)
    279              if not cname:
    280                  log.warning("invalid object name in '%s': '%s'"
    281                      % (self._filename, name))
    282                  continue
    283
    284              # discard duplicates
    285              if name in self._exact_matches:
    286                  continue
    287
    288              self._exact_matches[name] = url
    289              self._exact_matches[cname] = url
    290
    291              # Link the different ambiguous fragments to the url
    292              for i in range(1, len(cname)):
    293                  self._partial_names.setdefault(cname[i:], []).append(name)
    294
    295  #{ API register
    296  #  ------------
    297
    298  api_register = {}
    299  """
    300  Mapping from the API name to the `UrlGenerator` to be used.
    301
    302  Use `register_api()` to add new generators to the register.
    303  """
    304
    305 -def register_api(name, generator=None):
    306      """Register the API `name` into the `api_register`.
    307
    308      A registered API will be available to the markup as the interpreted text
    309      role ``name``.
    310
    311      If a `generator` is not provided, register a `VoidUrlGenerator` instance:
    312      in this case no warning will be issued for missing names, but no URL will
    313      be generated and all the dotted names will simply be rendered as literals.
    314
    315      :Parameters:
    316        `name` : `str`
    317          the name of the generator to be registered
    318        `generator` : `UrlGenerator`
    319          the object to register to translate names into URLs.
    320      """
    321      if generator is None:
    322          generator = VoidUrlGenerator()
    323
    324      api_register[name] = generator
    325
    326 -def set_api_file(name, file):
    327      """Set an URL generator populated with data from `file`.
    328
    329      Use `file` to populate a new `DocUrlGenerator` instance and register it
    330      as `name`.
    331
    332      :Parameters:
    333        `name` : `str`
    334          the name of the generator to be registered
    335        `file` : `str` or file
    336          the file to parse populate the URL generator
    337      """
    338      generator = DocUrlGenerator()
    339      generator.load_index(file)
    340      register_api(name, generator)
    341
    342 -def set_api_root(name, prefix):
    343      """Set the root for the URLs returned by a registered URL generator.
    344
    345      :Parameters:
    346        `name` : `str`
    347          the name of the generator to be updated
    348        `prefix` : `str`
    349          the prefix for the generated URL's
    350
    351      :Exceptions:
    352        - `IndexError`: `name` is not a registered generator
    353      """
    354      api_register[name].prefix = prefix
    355
    356  ######################################################################
    357  # Below this point requires docutils.
    358  try:
    359      import docutils
    360      from docutils.parsers.rst import roles
    361      from docutils import nodes, utils
    362      from docutils.readers.standalone import Reader
    363  except ImportError:
    364      docutils = roles = nodes = utils = None
    365 -    class Reader: settings_spec = ()
    366
    367 -def create_api_role(name, problematic):
    368      """
    369      Create and register a new role to create links for an API documentation.
    370
    371      Create a role called `name`, which will use the URL resolver registered as
    372      ``name`` in `api_register` to create a link for an object.
    373
    374      :Parameters:
    375        `name` : `str`
    376          name of the role to create.
    377        `problematic` : `bool`
    378          if True, the registered role will create problematic nodes in
    379          case of failed references. If False, a warning will be raised
    380          anyway, but the output will appear as an ordinary literal.
    381      """
    382      def resolve_api_name(n, rawtext, text, lineno, inliner,
    383                  options={}, content=[]):
    384          if docutils is None:
    385              raise AssertionError('requires docutils')
    386
    387          # node in monotype font
    388          text = utils.unescape(text)
    389          node = nodes.literal(rawtext, text, **options)
    390
    391          # Get the resolver from the register and create an url from it.
    392          try:
    393              url = api_register[name].get_url(text)
    394          except IndexError, exc:
    395              msg = inliner.reporter.warning(str(exc), line=lineno)
    396              if problematic:
    397                  prb = inliner.problematic(rawtext, text, msg)
    398                  return [prb], [msg]
    399              else:
    400                  return [node], []
    401
    402          if url is not None:
    403              node = nodes.reference(rawtext, '', node, refuri=url, **options)
    404          return [node], []
    405
    406      roles.register_local_role(name, resolve_api_name)
    407
    408
    409  #{ Command line parsing
    410  #  --------------------
    411
    412
    413 -def split_name(value):
    414      """
    415      Split an option in form ``NAME:VALUE`` and check if ``NAME`` exists.
    416      """
    417      parts = value.split(':', 1)
    418      if len(parts) != 2:
    419          raise OptionValueError(
    420              "option value must be specified as NAME:VALUE; got '%s' instead"
    421              % value)
    422
    423      name, val = parts
    424
    425      if name not in api_register:
    426          raise OptionValueError(
    427              "the name '%s' has not been registered; use --external-api"
    428              % name)
    429
    430      return (name, val)
    431
    432
    433 -class ApiLinkReader(Reader):
    434      """
    435      A Docutils standalone reader allowing external documentation links.
    436
    437      The reader configure the url resolvers at the time `read()` is invoked the
    438      first time.
    439      """
    440      #: The option parser configuration.
    441      settings_spec = (
    442      'API Linking Options',
    443      None,
    444      ((
    445          'Define a new API document.  A new interpreted text role NAME will be '
    446          'added.',
    447          ['--external-api'],
    448          {'metavar': 'NAME', 'action': 'append'}
    449      ), (
    450          'Use records in FILENAME to resolve objects in the API named NAME.',
    451          ['--external-api-file'],
    452          {'metavar': 'NAME:FILENAME', 'action': 'append'}
    453      ), (
    454          'Use STRING as prefix for the URL generated from the API NAME.',
    455          ['--external-api-root'],
    456          {'metavar': 'NAME:STRING', 'action': 'append'}
    457      ),)) + Reader.settings_spec
    458
    459 -    def __init__(self, *args, **kwargs):
    460          if docutils is None:
    461              raise AssertionError('requires docutils')
    462          Reader.__init__(self, *args, **kwargs)
    463
    464 -    def read(self, source, parser, settings):
    465          self.read_configuration(settings, problematic=True)
    466          return Reader.read(self, source, parser, settings)
    467
    468 -    def read_configuration(self, settings, problematic=True):
    469          """
    470          Read the configuration for the configured URL resolver.
    471
    472          Register a new role for each configured API.
    473
    474          :Parameters:
    475            `settings`
    476              the settings structure containing the options to read.
    477            `problematic` : `bool`
    478              if True, the registered role will create problematic nodes in
    479              case of failed references. If False, a warning will be raised
    480              anyway, but the output will appear as an ordinary literal.
    481          """
    482          # Read config only once
    483          if hasattr(self, '_conf'):
    484              return
    485          ApiLinkReader._conf = True
    486
    487          try:
    488              if settings.external_api is not None:
    489                  for name in settings.external_api:
    490                      register_api(name)
    491                      create_api_role(name, problematic=problematic)
    492
    493              if settings.external_api_file is not None:
    494                  for name, file in map(split_name, settings.external_api_file):
    495                      set_api_file(name, file)
    496
    497              if settings.external_api_root is not None:
    498                  for name, root in map(split_name, settings.external_api_root):
    499                      set_api_root(name, root)
    500
    501          except OptionValueError, exc:
    502              print >>sys.stderr, "%s: %s" % (exc.__class__.__name__, exc)
    503              sys.exit(2)
    504
    505      read_configuration = classmethod(read_configuration)
    506