mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-01-06 03:21:12 +07:00
506 lines
16 KiB
Python
506 lines
16 KiB
Python
"""
|
|
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)
|