Add <target> to one more testcase (see r8206).
[docutils.git] / sandbox / py-rest-doc / sphinx / directives.py
blob53310033ab9fff21dc3bd3cc8b882f5d458aded4
1 # -*- coding: utf-8 -*-
2 """
3 sphinx.directives
4 ~~~~~~~~~~~~~~~~~
6 Handlers for additional ReST directives.
8 :copyright: 2007 by Georg Brandl.
9 :license: Python license.
10 """
12 import re
13 import string
14 from os import path
16 from docutils import nodes
17 from docutils.parsers.rst import directives, roles
18 from docutils.parsers.rst.directives import admonitions
20 from . import addnodes
22 # ------ index markup --------------------------------------------------------------
24 entrytypes = [
25 'single', 'pair', 'triple', 'quadruple',
26 'module', 'keyword', 'operator', 'object', 'exception', 'statement', 'builtin',
29 def index_directive(name, arguments, options, content, lineno,
30 content_offset, block_text, state, state_machine):
31 arguments = arguments[0].split('\n')
32 env = state.document.settings.env
33 targetid = 'index-%s' % env.index_num
34 env.index_num += 1
35 targetnode = nodes.target('', '', ids=[targetid])
36 state.document.note_explicit_target(targetnode)
37 indexnode = addnodes.index()
38 indexnode['entries'] = arguments
39 for entry in arguments:
40 try:
41 type, string = entry.split(':', 1)
42 env.note_index_entry(type.strip(), string.strip(),
43 targetid, string.strip())
44 except ValueError:
45 continue
46 return [indexnode, targetnode]
48 index_directive.arguments = (1, 0, 1)
49 directives.register_directive('index', index_directive)
51 # ------ information units ---------------------------------------------------------
53 def desc_index_text(desctype, currmodule, name):
54 if desctype == 'function':
55 if not currmodule:
56 return '%s() (built-in function)' % name
57 return '%s() (in module %s)' % (name, currmodule)
58 elif desctype == 'data':
59 if not currmodule:
60 return '%s (built-in variable)' % name
61 return '%s (in module %s)' % (name, currmodule)
62 elif desctype == 'class':
63 return '%s (class in %s)' % (name, currmodule)
64 elif desctype == 'exception':
65 return name
66 elif desctype == 'method':
67 try:
68 clsname, methname = name.rsplit('.', 1)
69 except:
70 if currmodule:
71 return '%s() (in module %s)' % (name, currmodule)
72 else:
73 return '%s()' % name
74 if currmodule:
75 return '%s() (%s.%s method)' % (methname, currmodule, clsname)
76 else:
77 return '%s() (%s method)' % (methname, clsname)
78 elif desctype == 'attribute':
79 try:
80 clsname, attrname = name.rsplit('.', 1)
81 except:
82 if currmodule:
83 return '%s (in module %s)' % (name, currmodule)
84 else:
85 return name
86 if currmodule:
87 return '%s (%s.%s attribute)' % (attrname, currmodule, clsname)
88 else:
89 return '%s (%s attribute)' % (attrname, clsname)
90 elif desctype == 'opcode':
91 return '%s (opcode)' % name
92 elif desctype == 'cfunction':
93 return '%s (C function)' % name
94 elif desctype == 'cmember':
95 return '%s (C member)' % name
96 elif desctype == 'cmacro':
97 return '%s (C macro)' % name
98 elif desctype == 'ctype':
99 return '%s (C type)' % name
100 elif desctype == 'cvar':
101 return '%s (C variable)' % name
102 else:
103 raise ValueError("unhandled descenv: %s" % desctype)
106 # ------ functions to parse a Python or C signature and create desc_* nodes.
108 py_sig_re = re.compile(r'''^([\w.]*\.)? # class names
109 (\w+) \s* # thing name
110 (?: \((.*)\) )? $ # optionally arguments
111 ''', re.VERBOSE)
113 py_paramlist_re = re.compile(r'([\[\],])') # split at '[', ']' and ','
115 def parse_py_signature(signode, sig, desctype, currclass):
117 Transform a python signature into RST nodes. Returns (signode, fullname).
118 Return the fully qualified name of the thing.
120 If inside a class, the current class name is handled intelligently:
121 * it is stripped from the displayed name if present
122 * it is added to the full name (return value) if not present
124 m = py_sig_re.match(sig)
125 if m is None: raise ValueError
126 classname, name, arglist = m.groups()
128 if currclass:
129 if classname and classname.startswith(currclass):
130 fullname = classname + name
131 classname = classname[len(currclass):].lstrip('.')
132 elif classname:
133 fullname = currclass + '.' + classname + name
134 else:
135 fullname = currclass + '.' + name
136 else:
137 fullname = classname + name if classname else name
139 if classname:
140 signode += addnodes.desc_classname(classname, classname)
141 signode += addnodes.desc_name(name, name)
142 if not arglist:
143 if desctype in ('function', 'method'):
144 # for callables, add an empty parameter list
145 signode += addnodes.desc_parameterlist()
146 return fullname
147 signode += addnodes.desc_parameterlist()
149 stack = [signode[-1]]
150 arglist = arglist.replace('`', '').replace(r'\ ', '') # remove markup
151 for token in py_paramlist_re.split(arglist):
152 if token == '[':
153 opt = addnodes.desc_optional()
154 stack[-1] += opt
155 stack.append(opt)
156 elif token == ']':
157 try: stack.pop()
158 except IndexError: raise ValueError
159 elif not token or token == ',' or token.isspace():
160 pass
161 else:
162 token = token.strip()
163 stack[-1] += addnodes.desc_parameter(token, token)
164 if len(stack) != 1: raise ValueError
165 return fullname
168 c_sig_re = re.compile(
169 r'''^([^(]*?) # return type
170 (\w+) \s* # thing name
171 (?: \((.*)\) )? $ # optionally arguments
172 ''', re.VERBOSE)
173 c_funcptr_sig_re = re.compile(
174 r'''^([^(]+?) # return type
175 (\( [^()]+ \)) \s* # name in parentheses
176 \( (.*) \) $ # arguments
177 ''', re.VERBOSE)
179 # RE to split at word boundaries
180 wsplit_re = re.compile(r'(\W+)')
182 # These C types aren't described in the reference, so don't try to create
183 # a cross-reference to them
184 stopwords = set(('const', 'void', 'char', 'int', 'long', 'FILE', 'struct'))
186 def parse_c_type(node, ctype):
187 # add cross-ref nodes for all words
188 for part in filter(None, wsplit_re.split(ctype)):
189 tnode = nodes.Text(part, part)
190 if part[0] in string.letters+'_' and part not in stopwords:
191 pnode = addnodes.pending_xref(
192 '', reftype='ctype', reftarget=part, modname=None, classname=None)
193 pnode += tnode
194 node += pnode
195 else:
196 node += tnode
198 def parse_c_signature(signode, sig, desctype):
199 """Transform a C-language signature into RST nodes."""
200 # first try the function pointer signature regex, it's more specific
201 m = c_funcptr_sig_re.match(sig)
202 if m is None:
203 m = c_sig_re.match(sig)
204 if m is None:
205 raise ValueError('no match')
206 rettype, name, arglist = m.groups()
208 parse_c_type(signode, rettype)
209 signode += addnodes.desc_name(name, name)
210 if not arglist:
211 if desctype == 'cfunction':
212 # for functions, add an empty parameter list
213 signode += addnodes.desc_parameterlist()
214 return name
216 paramlist = addnodes.desc_parameterlist()
217 arglist = arglist.replace('`', '').replace('\\ ', '') # remove markup
218 # this messes up function pointer types, but not too badly ;)
219 args = arglist.split(',')
220 for arg in args:
221 arg = arg.strip()
222 param = addnodes.desc_parameter('', '', noemph=True)
223 try:
224 ctype, argname = arg.rsplit(' ', 1)
225 except ValueError:
226 # no argument name given, only the type
227 parse_c_type(param, arg)
228 else:
229 parse_c_type(param, ctype)
230 param += nodes.emphasis(' '+argname, ' '+argname)
231 paramlist += param
232 signode += paramlist
233 return name
236 opcode_sig_re = re.compile(r'(\w+(?:\+\d)?)\s*\((.*)\)')
238 def parse_opcode_signature(signode, sig, desctype):
239 """Transform an opcode signature into RST nodes."""
240 m = opcode_sig_re.match(sig)
241 if m is None: raise ValueError
242 opname, arglist = m.groups()
243 signode += addnodes.desc_name(opname, opname)
244 paramlist = addnodes.desc_parameterlist()
245 signode += paramlist
246 paramlist += addnodes.desc_parameter(arglist, arglist)
247 return opname.strip()
250 def add_refcount_annotation(env, node, name):
251 """Add a reference count annotation. Return None."""
252 entry = env.refcounts.get(name)
253 if not entry:
254 return
255 elif entry.result_type not in ("PyObject*", "PyVarObject*"):
256 return
257 rc = 'Return value: '
258 if entry.result_refs is None:
259 rc += "Always NULL."
260 else:
261 rc += ("New" if entry.result_refs else "Borrowed") + " reference."
262 node += addnodes.refcount(rc, rc)
265 def desc_directive(desctype, arguments, options, content, lineno,
266 content_offset, block_text, state, state_machine):
267 env = state.document.settings.env
268 node = addnodes.desc()
269 node['desctype'] = desctype
271 noindex = ('noindex' in options)
272 signatures = map(lambda s: s.strip(), arguments[0].split('\n'))
273 names = []
274 for i, sig in enumerate(signatures):
275 # add a signature node for each signature in the current unit
276 # and add a reference target for it
277 sig = sig.strip()
278 signode = addnodes.desc_signature(sig, '')
279 signode['first'] = False
280 node.append(signode)
281 try:
282 if desctype in ('function', 'data', 'class', 'exception',
283 'method', 'attribute'):
284 name = parse_py_signature(signode, sig, desctype, env.currclass)
285 elif desctype in ('cfunction', 'cmember', 'cmacro', 'ctype', 'cvar'):
286 name = parse_c_signature(signode, sig, desctype)
287 elif desctype == 'opcode':
288 name = parse_opcode_signature(signode, sig, desctype)
289 else:
290 # describe: use generic fallback
291 raise ValueError
292 except ValueError, err:
293 signode.clear()
294 signode += addnodes.desc_name(sig, sig)
295 continue # we don't want an index entry here
296 # only add target and index entry if this is the first description of the
297 # function name in this desc block
298 if not noindex and name not in names:
299 fullname = (env.currmodule + '.' if env.currmodule else '') + name
300 # note target
301 if fullname not in state.document.ids:
302 signode['names'].append(fullname)
303 signode['ids'].append(fullname)
304 signode['first'] = (not names)
305 state.document.note_explicit_target(signode)
306 env.note_descref(fullname, desctype)
307 names.append(name)
309 env.note_index_entry('single',
310 desc_index_text(desctype, env.currmodule, name),
311 fullname, fullname)
313 subnode = addnodes.desc_content()
314 if desctype == 'cfunction':
315 add_refcount_annotation(env, subnode, name)
316 # needed for automatic qualification of members
317 if desctype == 'class' and names:
318 env.currclass = names[0]
319 # needed for association of version{added,changed} directives
320 if names:
321 env.currdesc = names[0]
322 state.nested_parse(content, content_offset, subnode)
323 if desctype == 'class':
324 env.currclass = None
325 env.currdesc = None
326 node.append(subnode)
327 return [node]
329 desc_directive.content = 1
330 desc_directive.arguments = (1, 0, 1)
331 desc_directive.options = {'noindex': directives.flag}
333 desctypes = [
334 # the Python ones
335 'function',
336 'data',
337 'class',
338 'method',
339 'attribute',
340 'exception',
341 # the C ones
342 'cfunction',
343 'cmember',
344 'cmacro',
345 'ctype',
346 'cvar',
347 # the odd one
348 'opcode',
349 # the generic one
350 'describe',
353 for name in desctypes:
354 directives.register_directive(name, desc_directive)
357 # ------ versionadded/versionchanged -----------------------------------------------
359 def version_directive(name, arguments, options, content, lineno,
360 content_offset, block_text, state, state_machine):
361 node = addnodes.versionmodified()
362 node['type'] = name
363 node['version'] = arguments[0]
364 if len(arguments) == 2:
365 inodes, messages = state.inline_text(arguments[1], lineno+1)
366 node.extend(inodes)
367 if content:
368 state.nested_parse(content, content_offset, node)
369 ret = [node] + messages
370 else:
371 ret = [node]
372 env = state.document.settings.env
373 env.note_versionchange(node['type'], node['version'], node)
374 return ret
376 version_directive.arguments = (1, 1, 1)
377 version_directive.content = 1
379 directives.register_directive('deprecated', version_directive)
380 directives.register_directive('versionadded', version_directive)
381 directives.register_directive('versionchanged', version_directive)
384 # ------ see also ------------------------------------------------------------------
386 def seealso_directive(name, arguments, options, content, lineno,
387 content_offset, block_text, state, state_machine):
388 rv = admonitions.make_admonition(
389 addnodes.seealso, name, ['See also:'], options, content,
390 lineno, content_offset, block_text, state, state_machine)
391 return rv
393 seealso_directive.content = 1
394 seealso_directive.arguments = (0, 0, 0)
395 directives.register_directive('seealso', seealso_directive)
398 # ------ production list (for the reference) ---------------------------------------
400 def productionlist_directive(name, arguments, options, content, lineno,
401 content_offset, block_text, state, state_machine):
402 env = state.document.settings.env
403 node = addnodes.productionlist()
404 messages = []
405 i = 0
407 # use token as the default role while in production list
408 roles._roles[''] = roles._role_registry['token']
409 for rule in arguments[0].split('\n'):
410 if i == 0 and ':' not in rule:
411 # production group
412 continue
413 i += 1
414 try:
415 name, tokens = rule.split(':', 1)
416 except ValueError:
417 break
418 subnode = addnodes.production()
419 subnode['tokenname'] = name.strip()
420 if subnode['tokenname']:
421 idname = 'grammar-token-%s' % subnode['tokenname']
422 if idname not in state.document.ids:
423 subnode['ids'].append(idname)
424 state.document.note_implicit_target(subnode, subnode)
425 env.note_token(subnode['tokenname'])
426 inodes, imessages = state.inline_text(tokens, lineno+i)
427 subnode.extend(inodes)
428 messages.extend(imessages)
429 node.append(subnode)
430 del roles._roles['']
431 return [node] + messages
433 productionlist_directive.content = 0
434 productionlist_directive.arguments = (1, 0, 1)
435 directives.register_directive('productionlist', productionlist_directive)
437 # ------ section metadata ----------------------------------------------------------
439 def module_directive(name, arguments, options, content, lineno,
440 content_offset, block_text, state, state_machine):
441 env = state.document.settings.env
442 modname = arguments[0].strip()
443 env.currmodule = modname
444 env.note_module(modname, options.get('synopsis', ''), options.get('platform', ''))
445 ret = []
446 targetnode = nodes.target('', '', ids=['module-' + modname])
447 state.document.note_explicit_target(targetnode)
448 ret.append(targetnode)
449 if 'platform' in options:
450 node = nodes.paragraph()
451 node += nodes.emphasis('Platforms: ', 'Platforms: ')
452 node += nodes.Text(options['platform'], options['platform'])
453 ret.append(node)
454 # the synopsis isn't printed; in fact, it is only used in the modindex currently
455 env.note_index_entry('single', '%s (module)' % modname, 'module-' + modname,
456 modname)
457 return ret
459 module_directive.arguments = (1, 0, 0)
460 module_directive.options = {'platform': lambda x: x,
461 'synopsis': lambda x: x}
462 directives.register_directive('module', module_directive)
465 def author_directive(name, arguments, options, content, lineno,
466 content_offset, block_text, state, state_machine):
467 # The author directives aren't included in the built document
468 return []
470 author_directive.arguments = (1, 0, 1)
471 directives.register_directive('sectionauthor', author_directive)
472 directives.register_directive('moduleauthor', author_directive)
475 # ------ toctree directive ---------------------------------------------------------
477 def toctree_directive(name, arguments, options, content, lineno,
478 content_offset, block_text, state, state_machine):
479 env = state.document.settings.env
480 dirname = path.dirname(env.filename)
482 subnode = addnodes.toctree()
483 includefiles = filter(None, content)
484 # absolutize filenames
485 includefiles = map(lambda x: path.normpath(path.join(dirname, x)), includefiles)
486 subnode['includefiles'] = includefiles
487 subnode['maxdepth'] = options.get('maxdepth', -1)
488 return [subnode]
490 toctree_directive.content = 1
491 toctree_directive.options = {'maxdepth': int}
492 directives.register_directive('toctree', toctree_directive)
495 # ------ centered directive ---------------------------------------------------------
497 def centered_directive(name, arguments, options, content, lineno,
498 content_offset, block_text, state, state_machine):
499 if not arguments:
500 return []
501 subnode = addnodes.centered()
502 inodes, messages = state.inline_text(arguments[0], lineno)
503 subnode.extend(inodes)
504 return [subnode] + messages
506 centered_directive.arguments = (1, 0, 1)
507 directives.register_directive('centered', centered_directive)
510 # ------ highlightlanguage directive ------------------------------------------------
512 def highlightlang_directive(name, arguments, options, content, lineno,
513 content_offset, block_text, state, state_machine):
514 return [addnodes.highlightlang(lang=arguments[0].strip())]
516 highlightlang_directive.content = 0
517 highlightlang_directive.arguments = (1, 0, 0)
518 directives.register_directive('highlightlang',
519 highlightlang_directive)