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 1719 2008-02-15 01:00:39Z edloper $ 
 71  __version__ = "$Revision: 1719 $"[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 _TARGET_RE = re.compile(r'^(.*?)\s*<(?:URI:|URL:)?([^<>]+)>$') 368
369 -def create_api_role(name, problematic):
370 """ 371 Create and register a new role to create links for an API documentation. 372 373 Create a role called `name`, which will use the URL resolver registered as 374 ``name`` in `api_register` to create a link for an object. 375 376 :Parameters: 377 `name` : `str` 378 name of the role to create. 379 `problematic` : `bool` 380 if True, the registered role will create problematic nodes in 381 case of failed references. If False, a warning will be raised 382 anyway, but the output will appear as an ordinary literal. 383 """ 384 def resolve_api_name(n, rawtext, text, lineno, inliner, 385 options={}, content=[]): 386 if docutils is None: 387 raise AssertionError('requires docutils') 388 389 # Check if there's separate text & targets 390 m = _TARGET_RE.match(text) 391 if m: text, target = m.groups() 392 else: target = text 393 394 # node in monotype font 395 text = utils.unescape(text) 396 node = nodes.literal(rawtext, text, **options) 397 398 # Get the resolver from the register and create an url from it. 399 try: 400 url = api_register[name].get_url(target) 401 except IndexError, exc: 402 msg = inliner.reporter.warning(str(exc), line=lineno) 403 if problematic: 404 prb = inliner.problematic(rawtext, text, msg) 405 return [prb], [msg] 406 else: 407 return [node], [] 408 409 if url is not None: 410 node = nodes.reference(rawtext, '', node, refuri=url, **options) 411 return [node], []
412 413 roles.register_local_role(name, resolve_api_name) 414 415 416 #{ Command line parsing 417 # -------------------- 418 419
420 -def split_name(value):
421 """ 422 Split an option in form ``NAME:VALUE`` and check if ``NAME`` exists. 423 """ 424 parts = value.split(':', 1) 425 if len(parts) != 2: 426 raise OptionValueError( 427 "option value must be specified as NAME:VALUE; got '%s' instead" 428 % value) 429 430 name, val = parts 431 432 if name not in api_register: 433 raise OptionValueError( 434 "the name '%s' has not been registered; use --external-api" 435 % name) 436 437 return (name, val)
438 439
440 -class ApiLinkReader(Reader):
441 """ 442 A Docutils standalone reader allowing external documentation links. 443 444 The reader configure the url resolvers at the time `read()` is invoked the 445 first time. 446 """ 447 #: The option parser configuration. 448 settings_spec = ( 449 'API Linking Options', 450 None, 451 (( 452 'Define a new API document. A new interpreted text role NAME will be ' 453 'added.', 454 ['--external-api'], 455 {'metavar': 'NAME', 'action': 'append'} 456 ), ( 457 'Use records in FILENAME to resolve objects in the API named NAME.', 458 ['--external-api-file'], 459 {'metavar': 'NAME:FILENAME', 'action': 'append'} 460 ), ( 461 'Use STRING as prefix for the URL generated from the API NAME.', 462 ['--external-api-root'], 463 {'metavar': 'NAME:STRING', 'action': 'append'} 464 ),)) + Reader.settings_spec 465
466 - def __init__(self, *args, **kwargs):
467 if docutils is None: 468 raise AssertionError('requires docutils') 469 Reader.__init__(self, *args, **kwargs)
470
471 - def read(self, source, parser, settings):
472 self.read_configuration(settings, problematic=True) 473 return Reader.read(self, source, parser, settings)
474
475 - def read_configuration(self, settings, problematic=True):
476 """ 477 Read the configuration for the configured URL resolver. 478 479 Register a new role for each configured API. 480 481 :Parameters: 482 `settings` 483 the settings structure containing the options to read. 484 `problematic` : `bool` 485 if True, the registered role will create problematic nodes in 486 case of failed references. If False, a warning will be raised 487 anyway, but the output will appear as an ordinary literal. 488 """ 489 # Read config only once 490 if hasattr(self, '_conf'): 491 return 492 ApiLinkReader._conf = True 493 494 try: 495 if settings.external_api is not None: 496 for name in settings.external_api: 497 register_api(name) 498 create_api_role(name, problematic=problematic) 499 500 if settings.external_api_file is not None: 501 for name, file in map(split_name, settings.external_api_file): 502 set_api_file(name, file) 503 504 if settings.external_api_root is not None: 505 for name, root in map(split_name, settings.external_api_root): 506 set_api_root(name, root) 507 508 except OptionValueError, exc: 509 print >>sys.stderr, "%s: %s" % (exc.__class__.__name__, exc) 510 sys.exit(2)
511 512 read_configuration = classmethod(read_configuration)
513