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
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
83 """
84 Generate URL from an object name.
85 """
87 """
88 The name looked for is ambiguous
89 """
90
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
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
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 """
165
166
168 """
169 Read a *documentation index* and generate URL's for it.
170 """
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
208 cname = self.get_canonical_name(name)
209 url = self._exact_matches.get(cname, None)
210 if url is None:
211
212
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
229
230
232 """
233 Clear the current class content.
234 """
235 self._exact_matches.clear()
236 self._partial_names.clear()
237
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
256 """Iterate on a file returning 2-tuples."""
257 for nrow, row in enumerate(f):
258
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
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
285 if name in self._exact_matches:
286 continue
287
288 self._exact_matches[name] = url
289 self._exact_matches[cname] = url
290
291
292 for i in range(1, len(cname)):
293 self._partial_names.setdefault(cname[i:], []).append(name)
294
295
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
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
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
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
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
366
367 _TARGET_RE = re.compile(r'^(.*?)\s*<(?:URI:|URL:)?([^<>]+)>$')
368
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
390 m = _TARGET_RE.match(text)
391 if m: text, target = m.groups()
392 else: target = text
393
394
395 text = utils.unescape(text)
396 node = nodes.literal(rawtext, text, **options)
397
398
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
417
418
419
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
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
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
467 if docutils is None:
468 raise AssertionError('requires docutils')
469 Reader.__init__(self, *args, **kwargs)
470
471 - def read(self, source, parser, settings):
474
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
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