|
1 # -*- coding: utf-8 -*- |
|
2 """ |
|
3 sphinx.directives.desc |
|
4 ~~~~~~~~~~~~~~~~~~~~~~ |
|
5 |
|
6 :copyright: 2007-2008 by Georg Brandl. |
|
7 :license: BSD. |
|
8 """ |
|
9 |
|
10 import re |
|
11 import string |
|
12 |
|
13 from docutils import nodes |
|
14 from docutils.parsers.rst import directives |
|
15 |
|
16 from sphinx import addnodes |
|
17 from sphinx.util import ws_re |
|
18 |
|
19 |
|
20 # ------ information units --------------------------------------------------------- |
|
21 |
|
22 def desc_index_text(desctype, module, name, add_modules): |
|
23 if desctype == 'function': |
|
24 if not module: |
|
25 return _('%s() (built-in function)') % name |
|
26 return _('%s() (in module %s)') % (name, module) |
|
27 elif desctype == 'data': |
|
28 if not module: |
|
29 return _('%s (built-in variable)') % name |
|
30 return _('%s (in module %s)') % (name, module) |
|
31 elif desctype == 'class': |
|
32 if not module: |
|
33 return _('%s (built-in class)') % name |
|
34 return _('%s (class in %s)') % (name, module) |
|
35 elif desctype == 'exception': |
|
36 return name |
|
37 elif desctype == 'method': |
|
38 try: |
|
39 clsname, methname = name.rsplit('.', 1) |
|
40 except ValueError: |
|
41 if module: |
|
42 return _('%s() (in module %s)') % (name, module) |
|
43 else: |
|
44 return '%s()' % name |
|
45 if module and add_modules: |
|
46 return _('%s() (%s.%s method)') % (methname, module, clsname) |
|
47 else: |
|
48 return _('%s() (%s method)') % (methname, clsname) |
|
49 elif desctype == 'staticmethod': |
|
50 try: |
|
51 clsname, methname = name.rsplit('.', 1) |
|
52 except ValueError: |
|
53 if module: |
|
54 return _('%s() (in module %s)') % (name, module) |
|
55 else: |
|
56 return '%s()' % name |
|
57 if module and add_modules: |
|
58 return _('%s() (%s.%s static method)') % (methname, module, clsname) |
|
59 else: |
|
60 return _('%s() (%s static method)') % (methname, clsname) |
|
61 elif desctype == 'attribute': |
|
62 try: |
|
63 clsname, attrname = name.rsplit('.', 1) |
|
64 except ValueError: |
|
65 if module: |
|
66 return _('%s (in module %s)') % (name, module) |
|
67 else: |
|
68 return name |
|
69 if module and add_modules: |
|
70 return _('%s (%s.%s attribute)') % (attrname, module, clsname) |
|
71 else: |
|
72 return _('%s (%s attribute)') % (attrname, clsname) |
|
73 elif desctype == 'cfunction': |
|
74 return _('%s (C function)') % name |
|
75 elif desctype == 'cmember': |
|
76 return _('%s (C member)') % name |
|
77 elif desctype == 'cmacro': |
|
78 return _('%s (C macro)') % name |
|
79 elif desctype == 'ctype': |
|
80 return _('%s (C type)') % name |
|
81 elif desctype == 'cvar': |
|
82 return _('%s (C variable)') % name |
|
83 else: |
|
84 raise ValueError('unhandled descenv: %s' % desctype) |
|
85 |
|
86 |
|
87 # ------ make field lists (like :param foo:) in desc bodies prettier |
|
88 |
|
89 _ = lambda x: x # make gettext extraction in constants possible |
|
90 |
|
91 doc_fields_with_arg = { |
|
92 'param': '%param', |
|
93 'parameter': '%param', |
|
94 'arg': '%param', |
|
95 'argument': '%param', |
|
96 'keyword': '%param', |
|
97 'kwarg': '%param', |
|
98 'kwparam': '%param', |
|
99 'type': '%type', |
|
100 'raises': _('Raises'), |
|
101 'raise': 'Raises', |
|
102 'exception': 'Raises', |
|
103 'except': 'Raises', |
|
104 'var': _('Variable'), |
|
105 'ivar': 'Variable', |
|
106 'cvar': 'Variable', |
|
107 'returns': _('Returns'), |
|
108 'return': 'Returns', |
|
109 } |
|
110 |
|
111 doc_fields_with_linked_arg = ('raises', 'raise', 'exception', 'except') |
|
112 |
|
113 doc_fields_without_arg = { |
|
114 'returns': 'Returns', |
|
115 'return': 'Returns', |
|
116 'rtype': _('Return type'), |
|
117 } |
|
118 |
|
119 del _ |
|
120 |
|
121 |
|
122 def _is_only_paragraph(node): |
|
123 # determine if the node only contains one paragraph (and system messages) |
|
124 if len(node) == 0: |
|
125 return False |
|
126 elif len(node) > 1: |
|
127 for subnode in node[1:]: |
|
128 if not isinstance(subnode, nodes.system_message): |
|
129 return False |
|
130 if isinstance(node[0], nodes.paragraph): |
|
131 return True |
|
132 return False |
|
133 |
|
134 |
|
135 def handle_doc_fields(node, env): |
|
136 # don't traverse, only handle field lists that are immediate children |
|
137 for child in node.children: |
|
138 if not isinstance(child, nodes.field_list): |
|
139 continue |
|
140 params = None |
|
141 param_nodes = {} |
|
142 param_types = {} |
|
143 new_list = nodes.field_list() |
|
144 for field in child: |
|
145 fname, fbody = field |
|
146 try: |
|
147 typ, obj = fname.astext().split(None, 1) |
|
148 typdesc = _(doc_fields_with_arg[typ]) |
|
149 if _is_only_paragraph(fbody): |
|
150 children = fbody.children[0].children |
|
151 else: |
|
152 children = fbody.children |
|
153 if typdesc == '%param': |
|
154 if not params: |
|
155 pfield = nodes.field() |
|
156 pfield += nodes.field_name('', _('Parameters')) |
|
157 pfield += nodes.field_body() |
|
158 params = nodes.bullet_list() |
|
159 pfield[1] += params |
|
160 new_list += pfield |
|
161 dlitem = nodes.list_item() |
|
162 dlpar = nodes.paragraph() |
|
163 dlpar += nodes.emphasis(obj, obj) |
|
164 dlpar += nodes.Text(' -- ', ' -- ') |
|
165 dlpar += children |
|
166 param_nodes[obj] = dlpar |
|
167 dlitem += dlpar |
|
168 params += dlitem |
|
169 elif typdesc == '%type': |
|
170 typenodes = fbody.children |
|
171 if _is_only_paragraph(fbody): |
|
172 typenodes = [nodes.Text(' (')] + \ |
|
173 typenodes[0].children + [nodes.Text(')')] |
|
174 param_types[obj] = typenodes |
|
175 else: |
|
176 fieldname = typdesc + ' ' |
|
177 nfield = nodes.field() |
|
178 nfieldname = nodes.field_name(fieldname, fieldname) |
|
179 nfield += nfieldname |
|
180 node = nfieldname |
|
181 if typ in doc_fields_with_linked_arg: |
|
182 node = addnodes.pending_xref(obj, reftype='obj', |
|
183 refcaption=False, |
|
184 reftarget=obj, |
|
185 modname=env.currmodule, |
|
186 classname=env.currclass) |
|
187 nfieldname += node |
|
188 node += nodes.Text(obj, obj) |
|
189 nfield += nodes.field_body() |
|
190 nfield[1] += fbody.children |
|
191 new_list += nfield |
|
192 except (KeyError, ValueError): |
|
193 fnametext = fname.astext() |
|
194 try: |
|
195 typ = _(doc_fields_without_arg[fnametext]) |
|
196 except KeyError: |
|
197 # at least capitalize the field name |
|
198 typ = fnametext.capitalize() |
|
199 fname[0] = nodes.Text(typ) |
|
200 new_list += field |
|
201 for param, type in param_types.iteritems(): |
|
202 if param in param_nodes: |
|
203 param_nodes[param][1:1] = type |
|
204 child.replace_self(new_list) |
|
205 |
|
206 |
|
207 # ------ functions to parse a Python or C signature and create desc_* nodes. |
|
208 |
|
209 py_sig_re = re.compile( |
|
210 r'''^ ([\w.]*\.)? # class name(s) |
|
211 (\w+) \s* # thing name |
|
212 (?: \((.*)\) # optional arguments |
|
213 (\s* -> \s* .*)? )? $ # optional return annotation |
|
214 ''', re.VERBOSE) |
|
215 |
|
216 py_paramlist_re = re.compile(r'([\[\],])') # split at '[', ']' and ',' |
|
217 |
|
218 def parse_py_signature(signode, sig, desctype, module, env): |
|
219 """ |
|
220 Transform a python signature into RST nodes. |
|
221 Return (fully qualified name of the thing, classname if any). |
|
222 |
|
223 If inside a class, the current class name is handled intelligently: |
|
224 * it is stripped from the displayed name if present |
|
225 * it is added to the full name (return value) if not present |
|
226 """ |
|
227 m = py_sig_re.match(sig) |
|
228 if m is None: |
|
229 raise ValueError |
|
230 classname, name, arglist, retann = m.groups() |
|
231 |
|
232 if retann: |
|
233 retann = u' \N{RIGHTWARDS ARROW} ' + retann.strip()[2:] |
|
234 |
|
235 if env.currclass: |
|
236 add_module = False |
|
237 if classname and classname.startswith(env.currclass): |
|
238 fullname = classname + name |
|
239 # class name is given again in the signature |
|
240 classname = classname[len(env.currclass):].lstrip('.') |
|
241 elif classname: |
|
242 # class name is given in the signature, but different |
|
243 # (shouldn't happen) |
|
244 fullname = env.currclass + '.' + classname + name |
|
245 else: |
|
246 # class name is not given in the signature |
|
247 fullname = env.currclass + '.' + name |
|
248 else: |
|
249 add_module = True |
|
250 fullname = classname and classname + name or name |
|
251 |
|
252 if desctype == 'staticmethod': |
|
253 signode += addnodes.desc_annotation('static ', 'static ') |
|
254 |
|
255 if classname: |
|
256 signode += addnodes.desc_addname(classname, classname) |
|
257 # exceptions are a special case, since they are documented in the |
|
258 # 'exceptions' module. |
|
259 elif add_module and env.config.add_module_names and \ |
|
260 module and module != 'exceptions': |
|
261 nodetext = module + '.' |
|
262 signode += addnodes.desc_addname(nodetext, nodetext) |
|
263 |
|
264 signode += addnodes.desc_name(name, name) |
|
265 if not arglist: |
|
266 if desctype in ('function', 'method', 'staticmethod'): |
|
267 # for callables, add an empty parameter list |
|
268 signode += addnodes.desc_parameterlist() |
|
269 if retann: |
|
270 signode += addnodes.desc_type(retann, retann) |
|
271 return fullname, classname |
|
272 signode += addnodes.desc_parameterlist() |
|
273 |
|
274 stack = [signode[-1]] |
|
275 for token in py_paramlist_re.split(arglist): |
|
276 if token == '[': |
|
277 opt = addnodes.desc_optional() |
|
278 stack[-1] += opt |
|
279 stack.append(opt) |
|
280 elif token == ']': |
|
281 try: |
|
282 stack.pop() |
|
283 except IndexError: |
|
284 raise ValueError |
|
285 elif not token or token == ',' or token.isspace(): |
|
286 pass |
|
287 else: |
|
288 token = token.strip() |
|
289 stack[-1] += addnodes.desc_parameter(token, token) |
|
290 if len(stack) != 1: |
|
291 raise ValueError |
|
292 if retann: |
|
293 signode += addnodes.desc_type(retann, retann) |
|
294 return fullname, classname |
|
295 |
|
296 |
|
297 c_sig_re = re.compile( |
|
298 r'''^([^(]*?) # return type |
|
299 ([\w:]+) \s* # thing name (colon allowed for C++ class names) |
|
300 (?: \((.*)\) )? # optionally arguments |
|
301 (\s+const)? $ # const specifier |
|
302 ''', re.VERBOSE) |
|
303 c_funcptr_sig_re = re.compile( |
|
304 r'''^([^(]+?) # return type |
|
305 (\( [^()]+ \)) \s* # name in parentheses |
|
306 \( (.*) \) # arguments |
|
307 (\s+const)? $ # const specifier |
|
308 ''', re.VERBOSE) |
|
309 c_funcptr_name_re = re.compile(r'^\(\s*\*\s*(.*?)\s*\)$') |
|
310 |
|
311 # RE to split at word boundaries |
|
312 wsplit_re = re.compile(r'(\W+)') |
|
313 |
|
314 # These C types aren't described in the reference, so don't try to create |
|
315 # a cross-reference to them |
|
316 stopwords = set(('const', 'void', 'char', 'int', 'long', 'FILE', 'struct')) |
|
317 |
|
318 def parse_c_type(node, ctype): |
|
319 # add cross-ref nodes for all words |
|
320 for part in filter(None, wsplit_re.split(ctype)): |
|
321 tnode = nodes.Text(part, part) |
|
322 if part[0] in string.letters+'_' and part not in stopwords: |
|
323 pnode = addnodes.pending_xref( |
|
324 '', reftype='ctype', reftarget=part, modname=None, classname=None) |
|
325 pnode += tnode |
|
326 node += pnode |
|
327 else: |
|
328 node += tnode |
|
329 |
|
330 def parse_c_signature(signode, sig, desctype): |
|
331 """Transform a C (or C++) signature into RST nodes.""" |
|
332 # first try the function pointer signature regex, it's more specific |
|
333 m = c_funcptr_sig_re.match(sig) |
|
334 if m is None: |
|
335 m = c_sig_re.match(sig) |
|
336 if m is None: |
|
337 raise ValueError('no match') |
|
338 rettype, name, arglist, const = m.groups() |
|
339 |
|
340 signode += addnodes.desc_type('', '') |
|
341 parse_c_type(signode[-1], rettype) |
|
342 try: |
|
343 classname, funcname = name.split('::', 1) |
|
344 classname += '::' |
|
345 signode += addnodes.desc_addname(classname, classname) |
|
346 signode += addnodes.desc_name(funcname, funcname) |
|
347 # name (the full name) is still both parts |
|
348 except ValueError: |
|
349 signode += addnodes.desc_name(name, name) |
|
350 # clean up parentheses from canonical name |
|
351 m = c_funcptr_name_re.match(name) |
|
352 if m: |
|
353 name = m.group(1) |
|
354 if not arglist: |
|
355 if desctype == 'cfunction': |
|
356 # for functions, add an empty parameter list |
|
357 signode += addnodes.desc_parameterlist() |
|
358 return name |
|
359 |
|
360 paramlist = addnodes.desc_parameterlist() |
|
361 arglist = arglist.replace('`', '').replace('\\ ', '') # remove markup |
|
362 # this messes up function pointer types, but not too badly ;) |
|
363 args = arglist.split(',') |
|
364 for arg in args: |
|
365 arg = arg.strip() |
|
366 param = addnodes.desc_parameter('', '', noemph=True) |
|
367 try: |
|
368 ctype, argname = arg.rsplit(' ', 1) |
|
369 except ValueError: |
|
370 # no argument name given, only the type |
|
371 parse_c_type(param, arg) |
|
372 else: |
|
373 parse_c_type(param, ctype) |
|
374 param += nodes.emphasis(' '+argname, ' '+argname) |
|
375 paramlist += param |
|
376 signode += paramlist |
|
377 if const: |
|
378 signode += addnodes.desc_addname(const, const) |
|
379 return name |
|
380 |
|
381 |
|
382 option_desc_re = re.compile( |
|
383 r'((?:/|-|--)[-_a-zA-Z0-9]+)(\s*.*?)(?=,\s+(?:/|-|--)|$)') |
|
384 |
|
385 def parse_option_desc(signode, sig): |
|
386 """Transform an option description into RST nodes.""" |
|
387 count = 0 |
|
388 firstname = '' |
|
389 for m in option_desc_re.finditer(sig): |
|
390 optname, args = m.groups() |
|
391 if count: |
|
392 signode += addnodes.desc_addname(', ', ', ') |
|
393 signode += addnodes.desc_name(optname, optname) |
|
394 signode += addnodes.desc_addname(args, args) |
|
395 if not count: |
|
396 firstname = optname |
|
397 count += 1 |
|
398 if not firstname: |
|
399 raise ValueError |
|
400 return firstname |
|
401 |
|
402 |
|
403 strip_backslash_re = re.compile(r'\\(?=[^\\])') |
|
404 |
|
405 def desc_directive(desctype, arguments, options, content, lineno, |
|
406 content_offset, block_text, state, state_machine): |
|
407 env = state.document.settings.env |
|
408 inode = addnodes.index(entries=[]) |
|
409 node = addnodes.desc() |
|
410 node['desctype'] = desctype |
|
411 |
|
412 noindex = ('noindex' in options) |
|
413 node['noindex'] = noindex |
|
414 # remove backslashes to support (dummy) escapes; helps Vim's highlighting |
|
415 signatures = map(lambda s: strip_backslash_re.sub('', s.strip()), |
|
416 arguments[0].split('\n')) |
|
417 names = [] |
|
418 clsname = None |
|
419 module = options.get('module', env.currmodule) |
|
420 for i, sig in enumerate(signatures): |
|
421 # add a signature node for each signature in the current unit |
|
422 # and add a reference target for it |
|
423 sig = sig.strip() |
|
424 signode = addnodes.desc_signature(sig, '') |
|
425 signode['first'] = False |
|
426 node.append(signode) |
|
427 try: |
|
428 if desctype in ('function', 'data', 'class', 'exception', |
|
429 'method', 'staticmethod', 'attribute'): |
|
430 name, clsname = parse_py_signature(signode, sig, desctype, module, env) |
|
431 elif desctype in ('cfunction', 'cmember', 'cmacro', 'ctype', 'cvar'): |
|
432 name = parse_c_signature(signode, sig, desctype) |
|
433 elif desctype == 'cmdoption': |
|
434 optname = parse_option_desc(signode, sig) |
|
435 if not noindex: |
|
436 targetname = optname.replace('/', '-') |
|
437 if env.currprogram: |
|
438 targetname = '-' + env.currprogram + targetname |
|
439 targetname = 'cmdoption' + targetname |
|
440 signode['ids'].append(targetname) |
|
441 state.document.note_explicit_target(signode) |
|
442 inode['entries'].append( |
|
443 ('pair', _('%scommand line option; %s') % |
|
444 ((env.currprogram and env.currprogram + ' ' or ''), sig), |
|
445 targetname, targetname)) |
|
446 env.note_progoption(optname, targetname) |
|
447 continue |
|
448 elif desctype == 'describe': |
|
449 signode.clear() |
|
450 signode += addnodes.desc_name(sig, sig) |
|
451 continue |
|
452 else: |
|
453 # another registered generic x-ref directive |
|
454 rolename, indextemplate, parse_node = additional_xref_types[desctype] |
|
455 if parse_node: |
|
456 fullname = parse_node(env, sig, signode) |
|
457 else: |
|
458 signode.clear() |
|
459 signode += addnodes.desc_name(sig, sig) |
|
460 # normalize whitespace like xfileref_role does |
|
461 fullname = ws_re.sub('', sig) |
|
462 if not noindex: |
|
463 targetname = '%s-%s' % (rolename, fullname) |
|
464 signode['ids'].append(targetname) |
|
465 state.document.note_explicit_target(signode) |
|
466 if indextemplate: |
|
467 indexentry = _(indextemplate) % (fullname,) |
|
468 indextype = 'single' |
|
469 colon = indexentry.find(':') |
|
470 if colon != -1: |
|
471 indextype = indexentry[:colon].strip() |
|
472 indexentry = indexentry[colon+1:].strip() |
|
473 inode['entries'].append((indextype, indexentry, |
|
474 targetname, targetname)) |
|
475 env.note_reftarget(rolename, fullname, targetname) |
|
476 # don't use object indexing below |
|
477 continue |
|
478 except ValueError, err: |
|
479 # signature parsing failed |
|
480 signode.clear() |
|
481 signode += addnodes.desc_name(sig, sig) |
|
482 continue # we don't want an index entry here |
|
483 # only add target and index entry if this is the first description of the |
|
484 # function name in this desc block |
|
485 if not noindex and name not in names: |
|
486 fullname = (module and module + '.' or '') + name |
|
487 # note target |
|
488 if fullname not in state.document.ids: |
|
489 signode['names'].append(fullname) |
|
490 signode['ids'].append(fullname) |
|
491 signode['first'] = (not names) |
|
492 state.document.note_explicit_target(signode) |
|
493 env.note_descref(fullname, desctype, lineno) |
|
494 names.append(name) |
|
495 |
|
496 indextext = desc_index_text(desctype, module, name, |
|
497 env.config.add_module_names) |
|
498 inode['entries'].append(('single', indextext, fullname, fullname)) |
|
499 |
|
500 subnode = addnodes.desc_content() |
|
501 # needed for automatic qualification of members |
|
502 clsname_set = False |
|
503 if desctype in ('class', 'exception') and names: |
|
504 env.currclass = names[0] |
|
505 clsname_set = True |
|
506 elif desctype in ('method', 'staticmethod', 'attribute') and \ |
|
507 clsname and not env.currclass: |
|
508 env.currclass = clsname.strip('.') |
|
509 clsname_set = True |
|
510 # needed for association of version{added,changed} directives |
|
511 if names: |
|
512 env.currdesc = names[0] |
|
513 state.nested_parse(content, content_offset, subnode) |
|
514 handle_doc_fields(subnode, env) |
|
515 if clsname_set: |
|
516 env.currclass = None |
|
517 env.currdesc = None |
|
518 node.append(subnode) |
|
519 return [inode, node] |
|
520 |
|
521 desc_directive.content = 1 |
|
522 desc_directive.arguments = (1, 0, 1) |
|
523 desc_directive.options = {'noindex': directives.flag, |
|
524 'module': directives.unchanged} |
|
525 |
|
526 desctypes = [ |
|
527 # the Python ones |
|
528 'function', |
|
529 'data', |
|
530 'class', |
|
531 'method', |
|
532 'staticmethod', |
|
533 'attribute', |
|
534 'exception', |
|
535 # the C ones |
|
536 'cfunction', |
|
537 'cmember', |
|
538 'cmacro', |
|
539 'ctype', |
|
540 'cvar', |
|
541 # for command line options |
|
542 'cmdoption', |
|
543 # the generic one |
|
544 'describe', |
|
545 'envvar', |
|
546 ] |
|
547 |
|
548 for _name in desctypes: |
|
549 directives.register_directive(_name, desc_directive) |
|
550 |
|
551 _ = lambda x: x |
|
552 |
|
553 # Generic cross-reference types; they can be registered in the application; |
|
554 # the directives are either desc_directive or target_directive |
|
555 additional_xref_types = { |
|
556 # directive name: (role name, index text, function to parse the desc node) |
|
557 'envvar': ('envvar', _('environment variable; %s'), None), |
|
558 } |
|
559 |
|
560 del _ |
|
561 |
|
562 |
|
563 # ------ target -------------------------------------------------------------------- |
|
564 |
|
565 def target_directive(targettype, arguments, options, content, lineno, |
|
566 content_offset, block_text, state, state_machine): |
|
567 """Generic target for user-defined cross-reference types.""" |
|
568 env = state.document.settings.env |
|
569 rolename, indextemplate, foo = additional_xref_types[targettype] |
|
570 # normalize whitespace in fullname like xfileref_role does |
|
571 fullname = ws_re.sub('', arguments[0].strip()) |
|
572 targetname = '%s-%s' % (rolename, fullname) |
|
573 node = nodes.target('', '', ids=[targetname]) |
|
574 state.document.note_explicit_target(node) |
|
575 ret = [node] |
|
576 if indextemplate: |
|
577 indexentry = indextemplate % (fullname,) |
|
578 indextype = 'single' |
|
579 colon = indexentry.find(':') |
|
580 if colon != -1: |
|
581 indextype = indexentry[:colon].strip() |
|
582 indexentry = indexentry[colon+1:].strip() |
|
583 inode = addnodes.index(entries=[(indextype, indexentry, targetname, targetname)]) |
|
584 ret.insert(0, inode) |
|
585 env.note_reftarget(rolename, fullname, targetname) |
|
586 return ret |
|
587 |
|
588 target_directive.content = 0 |
|
589 target_directive.arguments = (1, 0, 1) |
|
590 |
|
591 # note, the target directive is not registered here, it is used by the application |
|
592 # when registering additional xref types |