| """ |
| A Docutils_ interpreted text role for cross-API reference support. |
| |
| This module allows a Docutils_ document to refer to elements defined in |
| external API documentation. It is possible to refer to many external API |
| from the same document. |
| |
| Each API documentation is assigned a new interpreted text role: using such |
| interpreted text, an user can specify an object name inside an API |
| documentation. The system will convert such text into an url and generate a |
| reference to it. For example, if the API ``db`` is defined, being a database |
| package, then a certain method may be referred as:: |
| |
| :db:`Connection.cursor()` |
| |
| To define a new API, an *index file* must be provided. This file contains |
| a mapping from the object name to the URL part required to resolve such object. |
| |
| Index file |
| ---------- |
| |
| Each line in the the index file describes an object. |
| |
| Each line contains the fully qualified name of the object and the URL at which |
| the documentation is located. The fields are separated by a ``<tab>`` |
| character. |
| |
| The URL's in the file are relative from the documentation root: the system can |
| be configured to add a prefix in front of each returned URL. |
| |
| Allowed names |
| ------------- |
| |
| When a name is used in an API text role, it is split over any *separator*. |
| The separators defined are '``.``', '``::``', '``->``'. All the text from the |
| first noise char (neither a separator nor alphanumeric or '``_``') is |
| discarded. The same algorithm is applied when the index file is read. |
| |
| First the sequence of name parts is looked for in the provided index file. |
| If no matching name is found, a partial match against the trailing part of the |
| names in the index is performed. If no object is found, or if the trailing part |
| of the name may refer to many objects, a warning is issued and no reference |
| is created. |
| |
| Configuration |
| ------------- |
| |
| This module provides the class `ApiLinkReader` a replacement for the Docutils |
| standalone reader. Such reader specifies the settings required for the |
| API canonical roles configuration. The same command line options are exposed by |
| Epydoc. |
| |
| The script ``apirst2html.py`` is a frontend for the `ApiLinkReader` reader. |
| |
| API Linking Options:: |
| |
| --external-api=NAME |
| Define a new API document. A new interpreted text |
| role NAME will be added. |
| --external-api-file=NAME:FILENAME |
| Use records in FILENAME to resolve objects in the API |
| named NAME. |
| --external-api-root=NAME:STRING |
| Use STRING as prefix for the URL generated from the |
| API NAME. |
| |
| .. _Docutils: http://docutils.sourceforge.net/ |
| """ |
| |
| # $Id: xlink.py 1586 2007-03-14 01:53:42Z dvarrazzo $ |
| __version__ = "$Revision: 1586 $"[11:-2] |
| __author__ = "Daniele Varrazzo" |
| __copyright__ = "Copyright (C) 2007 by Daniele Varrazzo" |
| __docformat__ = 'reStructuredText en' |
| |
| import re |
| import sys |
| from optparse import OptionValueError |
| |
| from epydoc import log |
| |
| class UrlGenerator: |
| """ |
| Generate URL from an object name. |
| """ |
| class IndexAmbiguous(IndexError): |
| """ |
| The name looked for is ambiguous |
| """ |
| |
| def get_url(self, name): |
| """Look for a name and return the matching URL documentation. |
| |
| First look for a fully qualified name. If not found, try with partial |
| name. |
| |
| If no url exists for the given object, return `None`. |
| |
| :Parameters: |
| `name` : `str` |
| the name to look for |
| |
| :return: the URL that can be used to reach the `name` documentation. |
| `None` if no such URL exists. |
| :rtype: `str` |
| |
| :Exceptions: |
| - `IndexError`: no object found with `name` |
| - `DocUrlGenerator.IndexAmbiguous` : more than one object found with |
| a non-fully qualified name; notice that this is an ``IndexError`` |
| subclass |
| """ |
| raise NotImplementedError |
| |
| def get_canonical_name(self, name): |
| """ |
| Convert an object name into a canonical name. |
| |
| the canonical name of an object is a tuple of strings containing its |
| name fragments, splitted on any allowed separator ('``.``', '``::``', |
| '``->``'). |
| |
| Noise such parenthesis to indicate a function is discarded. |
| |
| :Parameters: |
| `name` : `str` |
| an object name, such as ``os.path.prefix()`` or ``lib::foo::bar`` |
| |
| :return: the fully qualified name such ``('os', 'path', 'prefix')`` and |
| ``('lib', 'foo', 'bar')`` |
| :rtype: `tuple` of `str` |
| """ |
| rv = [] |
| for m in self._SEP_RE.finditer(name): |
| groups = m.groups() |
| if groups[0] is not None: |
| rv.append(groups[0]) |
| elif groups[2] is not None: |
| break |
| |
| return tuple(rv) |
| |
| _SEP_RE = re.compile(r"""(?x) |
| # Tokenize the input into keyword, separator, noise |
| ([a-zA-Z0-9_]+) | # A keyword is a alphanum word |
| ( \. | \:\: | \-\> ) | # These are the allowed separators |
| (.) # If it doesn't fit, it's noise. |
| # Matching a single noise char is enough, because it |
| # is used to break the tokenization as soon as some noise |
| # is found. |
| """) |
| |
| |
| class VoidUrlGenerator(UrlGenerator): |
| """ |
| Don't actually know any url, but don't report any error. |
| |
| Useful if an index file is not available, but a document linking to it |
| is to be generated, and warnings are to be avoided. |
| |
| Don't report any object as missing, Don't return any url anyway. |
| """ |
| def get_url(self, name): |
| return None |
| |
| |
| class DocUrlGenerator(UrlGenerator): |
| """ |
| Read a *documentation index* and generate URL's for it. |
| """ |
| def __init__(self): |
| self._exact_matches = {} |
| """ |
| A map from an object fully qualified name to its URL. |
| |
| Values are both the name as tuple of fragments and as read from the |
| records (see `load_records()`), mostly to help `_partial_names` to |
| perform lookup for unambiguous names. |
| """ |
| |
| self._partial_names= {} |
| """ |
| A map from partial names to the fully qualified names they may refer. |
| |
| The keys are the possible left sub-tuples of fully qualified names, |
| the values are list of strings as provided by the index. |
| |
| If the list for a given tuple contains a single item, the partial |
| match is not ambuguous. In this case the string can be looked up in |
| `_exact_matches`. |
| |
| If the name fragment is ambiguous, a warning may be issued to the user. |
| The items can be used to provide an informative message to the user, |
| to help him qualifying the name in a unambiguous manner. |
| """ |
| |
| self.prefix = '' |
| """ |
| Prefix portion for the URL's returned by `get_url()`. |
| """ |
| |
| self._filename = None |
| """ |
| Not very important: only for logging. |
| """ |
| |
| def get_url(self, name): |
| cname = self.get_canonical_name(name) |
| url = self._exact_matches.get(cname, None) |
| if url is None: |
| |
| # go for a partial match |
| vals = self._partial_names.get(cname) |
| if vals is None: |
| raise IndexError( |
| "no object named '%s' found" % (name)) |
| |
| elif len(vals) == 1: |
| url = self._exact_matches[vals[0]] |
| |
| else: |
| raise self.IndexAmbiguous( |
| "found %d objects that '%s' may refer to: %s" |
| % (len(vals), name, ", ".join(["'%s'" % n for n in vals]))) |
| |
| return self.prefix + url |
| |
| #{ Content loading |
| # --------------- |
| |
| def clear(self): |
| """ |
| Clear the current class content. |
| """ |
| self._exact_matches.clear() |
| self._partial_names.clear() |
| |
| def load_index(self, f): |
| """ |
| Read the content of an index file. |
| |
| Populate the internal maps with the file content using `load_records()`. |
| |
| :Parameters: |
| f : `str` or file |
| a file name or file-like object fron which read the index. |
| """ |
| self._filename = str(f) |
| |
| if isinstance(f, basestring): |
| f = open(f) |
| |
| self.load_records(self._iter_tuples(f)) |
| |
| def _iter_tuples(self, f): |
| """Iterate on a file returning 2-tuples.""" |
| for nrow, row in enumerate(f): |
| # skip blank lines |
| row = row.rstrip() |
| if not row: continue |
| |
| rec = row.split('\t', 2) |
| if len(rec) == 2: |
| yield rec |
| else: |
| log.warning("invalid row in '%s' row %d: '%s'" |
| % (self._filename, nrow+1, row)) |
| |
| def load_records(self, records): |
| """ |
| Read a sequence of pairs name -> url and populate the internal maps. |
| |
| :Parameters: |
| records : iterable |
| the sequence of pairs (*name*, *url*) to add to the maps. |
| """ |
| for name, url in records: |
| cname = self.get_canonical_name(name) |
| if not cname: |
| log.warning("invalid object name in '%s': '%s'" |
| % (self._filename, name)) |
| continue |
| |
| # discard duplicates |
| if name in self._exact_matches: |
| continue |
| |
| self._exact_matches[name] = url |
| self._exact_matches[cname] = url |
| |
| # Link the different ambiguous fragments to the url |
| for i in range(1, len(cname)): |
| self._partial_names.setdefault(cname[i:], []).append(name) |
| |
| #{ API register |
| # ------------ |
| |
| api_register = {} |
| """ |
| Mapping from the API name to the `UrlGenerator` to be used. |
| |
| Use `register_api()` to add new generators to the register. |
| """ |
| |
| def register_api(name, generator=None): |
| """Register the API `name` into the `api_register`. |
| |
| A registered API will be available to the markup as the interpreted text |
| role ``name``. |
| |
| If a `generator` is not provided, register a `VoidUrlGenerator` instance: |
| in this case no warning will be issued for missing names, but no URL will |
| be generated and all the dotted names will simply be rendered as literals. |
| |
| :Parameters: |
| `name` : `str` |
| the name of the generator to be registered |
| `generator` : `UrlGenerator` |
| the object to register to translate names into URLs. |
| """ |
| if generator is None: |
| generator = VoidUrlGenerator() |
| |
| api_register[name] = generator |
| |
| def set_api_file(name, file): |
| """Set an URL generator populated with data from `file`. |
| |
| Use `file` to populate a new `DocUrlGenerator` instance and register it |
| as `name`. |
| |
| :Parameters: |
| `name` : `str` |
| the name of the generator to be registered |
| `file` : `str` or file |
| the file to parse populate the URL generator |
| """ |
| generator = DocUrlGenerator() |
| generator.load_index(file) |
| register_api(name, generator) |
| |
| def set_api_root(name, prefix): |
| """Set the root for the URLs returned by a registered URL generator. |
| |
| :Parameters: |
| `name` : `str` |
| the name of the generator to be updated |
| `prefix` : `str` |
| the prefix for the generated URL's |
| |
| :Exceptions: |
| - `IndexError`: `name` is not a registered generator |
| """ |
| api_register[name].prefix = prefix |
| |
| ###################################################################### |
| # Below this point requires docutils. |
| try: |
| import docutils |
| from docutils.parsers.rst import roles |
| from docutils import nodes, utils |
| from docutils.readers.standalone import Reader |
| except ImportError: |
| docutils = roles = nodes = utils = None |
| class Reader: settings_spec = () |
| |
| def create_api_role(name, problematic): |
| """ |
| Create and register a new role to create links for an API documentation. |
| |
| Create a role called `name`, which will use the URL resolver registered as |
| ``name`` in `api_register` to create a link for an object. |
| |
| :Parameters: |
| `name` : `str` |
| name of the role to create. |
| `problematic` : `bool` |
| if True, the registered role will create problematic nodes in |
| case of failed references. If False, a warning will be raised |
| anyway, but the output will appear as an ordinary literal. |
| """ |
| def resolve_api_name(n, rawtext, text, lineno, inliner, |
| options={}, content=[]): |
| if docutils is None: |
| raise AssertionError('requires docutils') |
| |
| # node in monotype font |
| text = utils.unescape(text) |
| node = nodes.literal(rawtext, text, **options) |
| |
| # Get the resolver from the register and create an url from it. |
| try: |
| url = api_register[name].get_url(text) |
| except IndexError, exc: |
| msg = inliner.reporter.warning(str(exc), line=lineno) |
| if problematic: |
| prb = inliner.problematic(rawtext, text, msg) |
| return [prb], [msg] |
| else: |
| return [node], [] |
| |
| if url is not None: |
| node = nodes.reference(rawtext, '', node, refuri=url, **options) |
| return [node], [] |
| |
| roles.register_local_role(name, resolve_api_name) |
| |
| |
| #{ Command line parsing |
| # -------------------- |
| |
| |
| def split_name(value): |
| """ |
| Split an option in form ``NAME:VALUE`` and check if ``NAME`` exists. |
| """ |
| parts = value.split(':', 1) |
| if len(parts) != 2: |
| raise OptionValueError( |
| "option value must be specified as NAME:VALUE; got '%s' instead" |
| % value) |
| |
| name, val = parts |
| |
| if name not in api_register: |
| raise OptionValueError( |
| "the name '%s' has not been registered; use --external-api" |
| % name) |
| |
| return (name, val) |
| |
| |
| class ApiLinkReader(Reader): |
| """ |
| A Docutils standalone reader allowing external documentation links. |
| |
| The reader configure the url resolvers at the time `read()` is invoked the |
| first time. |
| """ |
| #: The option parser configuration. |
| settings_spec = ( |
| 'API Linking Options', |
| None, |
| (( |
| 'Define a new API document. A new interpreted text role NAME will be ' |
| 'added.', |
| ['--external-api'], |
| {'metavar': 'NAME', 'action': 'append'} |
| ), ( |
| 'Use records in FILENAME to resolve objects in the API named NAME.', |
| ['--external-api-file'], |
| {'metavar': 'NAME:FILENAME', 'action': 'append'} |
| ), ( |
| 'Use STRING as prefix for the URL generated from the API NAME.', |
| ['--external-api-root'], |
| {'metavar': 'NAME:STRING', 'action': 'append'} |
| ),)) + Reader.settings_spec |
| |
| def __init__(self, *args, **kwargs): |
| if docutils is None: |
| raise AssertionError('requires docutils') |
| Reader.__init__(self, *args, **kwargs) |
| |
| def read(self, source, parser, settings): |
| self.read_configuration(settings, problematic=True) |
| return Reader.read(self, source, parser, settings) |
| |
| def read_configuration(self, settings, problematic=True): |
| """ |
| Read the configuration for the configured URL resolver. |
| |
| Register a new role for each configured API. |
| |
| :Parameters: |
| `settings` |
| the settings structure containing the options to read. |
| `problematic` : `bool` |
| if True, the registered role will create problematic nodes in |
| case of failed references. If False, a warning will be raised |
| anyway, but the output will appear as an ordinary literal. |
| """ |
| # Read config only once |
| if hasattr(self, '_conf'): |
| return |
| ApiLinkReader._conf = True |
| |
| try: |
| if settings.external_api is not None: |
| for name in settings.external_api: |
| register_api(name) |
| create_api_role(name, problematic=problematic) |
| |
| if settings.external_api_file is not None: |
| for name, file in map(split_name, settings.external_api_file): |
| set_api_file(name, file) |
| |
| if settings.external_api_root is not None: |
| for name, root in map(split_name, settings.external_api_root): |
| set_api_root(name, root) |
| |
| except OptionValueError, exc: |
| print >>sys.stderr, "%s: %s" % (exc.__class__.__name__, exc) |
| sys.exit(2) |
| |
| read_configuration = classmethod(read_configuration) |