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: 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
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
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
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
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
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
364
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
378 text = utils.unescape(text)
379 node = nodes.literal(rawtext, text, **options)
380
381
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
400
401
402
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
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
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
450 if docutils is None:
451 raise AssertionError('requires docutils')
452 Reader.__init__(self, *args, **kwargs)
453
454 - def read(self, source, parser, settings):
457
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
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