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