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 1556 2007-02-27 05:16:41Z edloper $ 
 71  __version__ = "$Revision: 1556 $"[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
303 -def register_api(name, generator=None):
304 """Register the API `name` into the `api_register`. 305 306 A registered API is available to the markup as the interpreted text 307 role ``name``. 308 309 If a `generator` is not provided, register a `VoidUrlGenerator` instance: 310 in this case no warning will be issued for missing names, but no URL will 311 be generated and all the dotted names will simply be rendered as literals. 312 313 :Parameters: 314 `name` : `str` 315 the name of the generator to be registered 316 `generator` : `UrlGenerator` 317 the object to register to translate names into URLs. 318 """ 319 if generator is None: 320 generator = VoidUrlGenerator() 321 322 api_register[name] = generator
323
324 -def set_api_file(name, file):
325 """Set an URL generator populated with data from `file`. 326 327 Use `file` to populate a new `DocUrlGenerator` instance and register it 328 as `name`. 329 330 :Parameters: 331 `name` : `str` 332 the name of the generator to be registered 333 `file` : `str` or file 334 the file to parse populate the URL generator 335 """ 336 generator = DocUrlGenerator() 337 generator.load_index(file) 338 register_api(name, generator)
339
340 -def set_api_root(name, prefix):
341 """Set the root for the URLs returned by a registered URL generator. 342 343 :Parameters: 344 `name` : `str` 345 the name of the generator to be updated 346 `prefix` : `str` 347 the prefix for the generated URL's 348 349 :Exceptions: 350 - `IndexError`: `name` is not a registered generator 351 """ 352 api_register[name].prefix = prefix
353 354 ###################################################################### 355 # Below this point requires docutils. 356 try: 357 import docutils 358 from docutils.parsers.rst import roles 359 from docutils import nodes, utils 360 from docutils.readers.standalone import Reader 361 except ImportError: 362 docutils = roles = nodes = utils = None
363 - class Reader: settings_spec = ()
364
365 -def create_api_role(name, problematic):
366 """ 367 Create and register a new role to create links for an API documentation. 368 369 Create a role called `name`, which will use the ``name`` registered 370 URL resolver to create a link for an object. 371 """ 372 def resolve_api_name(n, rawtext, text, lineno, inliner, 373 options={}, content=[]): 374 if docutils is None: 375 raise AssertionError('requires docutils') 376 377 # node in monotype font 378 text = utils.unescape(text) 379 node = nodes.literal(rawtext, text, **options) 380 381 # Get the resolver from the register and create an url from it. 382 try: 383 url = api_register[name].get_url(text) 384 except IndexError, exc: 385 msg = inliner.reporter.warning(str(exc), line=lineno) 386 if problematic: 387 prb = inliner.problematic(rawtext, text, msg) 388 return [prb], [msg] 389 else: 390 return [node], [] 391 392 if url is not None: 393 node = nodes.reference(rawtext, '', node, refuri=url, **options) 394 return [node], []
395 396 roles.register_local_role(name, resolve_api_name) 397 398 399 #{ Command line parsing 400 # -------------------- 401 402
403 -def split_name(value):
404 """ 405 Split an option in form ``NAME:VALUE`` and check if ``NAME`` exists. 406 """ 407 parts = value.split(':', 1) 408 if len(parts) != 2: 409 raise OptionValueError( 410 "option value must be specified as NAME:VALUE; got '%s' instead" 411 % value) 412 413 name, val = parts 414 415 if name not in api_register: 416 raise OptionValueError( 417 "the name '%s' has not been registered; use --external-api" 418 % name) 419 420 return (name, val)
421 422
423 -class ApiLinkReader(Reader):
424 """ 425 A Docutils standalone reader allowing external documentation links. 426 427 The reader configure the url resolvers at the time `read()` is invoked the 428 first time. 429 """ 430 #: The option parser configuration. 431 settings_spec = ( 432 'API Linking Options', 433 None, 434 (( 435 'Define a new API document. A new interpreted text role NAME will be ' 436 'added.', 437 ['--external-api'], 438 {'metavar': 'NAME', 'action': 'append'} 439 ), ( 440 'Use records in FILENAME to resolve objects in the API named NAME.', 441 ['--external-api-file'], 442 {'metavar': 'NAME:FILENAME', 'action': 'append'} 443 ), ( 444 'Use STRING as prefix for the URL generated from the API NAME.', 445 ['--external-api-root'], 446 {'metavar': 'NAME:STRING', 'action': 'append'} 447 ),)) + Reader.settings_spec 448
449 - def __init__(self, *args, **kwargs):
450 if docutils is None: 451 raise AssertionError('requires docutils') 452 Reader.__init__(self, *args, **kwargs)
453
454 - def read(self, source, parser, settings):
455 self.read_configuration(settings, problematic=True) 456 return Reader.read(self, source, parser, settings)
457
458 - def read_configuration(self, settings, problematic=True):
459 """ 460 Read the configuration for the configured URL resolver. 461 462 Register a new role for each configured API. 463 464 :Parameters: 465 `settings` 466 the settings structure containing the options to read. 467 `problematic` : `bool` 468 if True, the registered role will create problematic nodes in 469 case of failed references. If False, a warning will be raised 470 anyway, but the output will appear as an ordinary literal. 471 """ 472 # Read config only once 473 if hasattr(self, '_conf'): 474 return 475 ApiLinkReader._conf = True 476 477 try: 478 if settings.external_api is not None: 479 for name in settings.external_api: 480 register_api(name) 481 create_api_role(name, problematic=problematic) 482 483 if settings.external_api_file is not None: 484 for name, file in map(split_name, settings.external_api_file): 485 set_api_file(name, file) 486 487 if settings.external_api_root is not None: 488 for name, root in map(split_name, settings.external_api_root): 489 set_api_root(name, root) 490 491 except OptionValueError, exc: 492 print >>sys.stderr, "%s: %s" % (exc.__class__.__name__, exc) 493 sys.exit(2)
494 495 read_configuration = classmethod(read_configuration)
496