]> git.sesse.net Git - vlc/blob - bindings/python-ctypes/generate.py
python-ctypes: improve documentation generation
[vlc] / bindings / python-ctypes / generate.py
1 #! /usr/bin/python
2 debug=False
3
4 #
5 # Code generator for python ctypes bindings for VLC
6 # Copyright (C) 2009 the VideoLAN team
7 # $Id: $
8 #
9 # Authors: Olivier Aubert <olivier.aubert at liris.cnrs.fr>
10 #
11 # This program is free software; you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation; either version 2 of the License, or
14 # (at your option) any later version.
15 #
16 # This program is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19 # GNU General Public License for more details.
20 #
21 # You should have received a copy of the GNU General Public License
22 # along with this program; if not, write to the Free Software
23 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
24 #
25
26 """This module parses VLC public API include files and generates
27 corresponding python/ctypes code. Moreover, it generates class
28 wrappers for most methods.
29 """
30
31 import sys
32 import re
33 import time
34 import operator
35 import itertools
36
37 # DefaultDict from ASPN python cookbook
38 import copy
39 class DefaultDict(dict):
40     """Dictionary with a default value for unknown keys."""
41     def __init__(self, default=None, **items):
42         dict.__init__(self, **items)
43         self.default = default
44
45     def __getitem__(self, key):
46         if key in self:
47             return self.get(key)
48         else:
49             ## Need copy in case self.default is something like []
50             return self.setdefault(key, copy.deepcopy(self.default))
51
52     def __copy__(self):
53         return DefaultDict(self.default, **self)
54
55 # Methods not decorated/not referenced
56 blacklist=[
57     "libvlc_exception_raise",
58     "libvlc_exception_raised",
59     "libvlc_exception_get_message",
60     "libvlc_get_vlc_instance",
61
62     "libvlc_media_add_option_flag",
63     "libvlc_media_list_view_index_of_item",
64     "libvlc_media_list_view_insert_at_index",
65     "libvlc_media_list_view_remove_at_index",
66     "libvlc_media_list_view_add_item",
67
68     # In svn but not in current 1.0.0
69     'libvlc_video_set_deinterlace',
70     'libvlc_video_get_marquee_option_as_int',
71     'libvlc_video_get_marquee_option_as_string',
72     'libvlc_video_set_marquee_option_as_int',
73     'libvlc_video_set_marquee_option_as_string',
74     'libvlc_vlm_get_event_manager',
75
76     'mediacontrol_PlaylistSeq__free',
77
78     # TODO
79     "libvlc_event_detach",
80     "libvlc_event_attach",
81     ]
82
83 # Precompiled regexps
84 api_re=re.compile('VLC_PUBLIC_API\s+(\S+\s+.+?)\s*\(\s*(.+?)\s*\)')
85 param_re=re.compile('\s*(const\s*|unsigned\s*|struct\s*)?(\S+\s*\**)\s+(.+)')
86 paramlist_re=re.compile('\s*,\s*')
87 comment_re=re.compile('\\param\s+(\S+)')
88 python_param_re=re.compile('(@param\s+\S+)(.+)')
89 forward_re=re.compile('.+\(\s*(.+?)\s*\)(\s*\S+)')
90 enum_re=re.compile('typedef\s+(enum)\s*(\S+\s*)?\{\s*(.+)\s*\}\s*(\S+);')
91
92 # Definition of parameter passing mode for types.  This should not be
93 # hardcoded this way, but works alright ATM.
94 parameter_passing=DefaultDict(default=1)
95 parameter_passing['libvlc_exception_t*']=3
96
97 # C-type to ctypes/python type conversion.
98 # Note that enum types conversions are generated (cf convert_enum_names)
99 typ2class={
100     'libvlc_exception_t*': 'ctypes.POINTER(VLCException)',
101
102     'libvlc_media_player_t*': 'MediaPlayer',
103     'libvlc_instance_t*': 'Instance',
104     'libvlc_media_t*': 'Media',
105     'libvlc_log_t*': 'Log',
106     'libvlc_log_iterator_t*': 'LogIterator',
107     'libvlc_log_message_t*': 'LogMessage',
108     'libvlc_event_type_t': 'EventType',
109     'libvlc_event_manager_t*': 'EventManager',
110     'libvlc_media_discoverer_t*': 'MediaDiscoverer',
111     'libvlc_media_library_t*': 'MediaLibrary',
112     'libvlc_media_list_t*': 'MediaList',
113     'libvlc_media_list_player_t*': 'MediaListPlayer',
114     'libvlc_media_list_view_t*': 'MediaListView',
115     'libvlc_track_description_t*': 'TrackDescription',
116     'libvlc_audio_output_t*': 'AudioOutput',
117
118     'mediacontrol_Instance*': 'MediaControl',
119     'mediacontrol_Exception*': 'MediaControlException',
120     'mediacontrol_RGBPicture*': 'RGBPicture',
121     'mediacontrol_PlaylistSeq*': 'MediaControlPlaylistSeq',
122     'mediacontrol_Position*': 'MediaControlPosition',
123     'mediacontrol_StreamInformation*': 'MediaControlStreamInformation',
124     'WINDOWHANDLE': 'ctypes.c_ulong',
125
126     'void': 'None',
127     'void*': 'ctypes.c_void_p',
128     'short': 'ctypes.c_short',
129     'char*': 'ctypes.c_char_p',
130     'char**': 'ListPOINTER(ctypes.c_char_p)',
131     'uint32_t': 'ctypes.c_uint',
132     'float': 'ctypes.c_float',
133     'unsigned': 'ctypes.c_uint',
134     'int': 'ctypes.c_int',
135     '...': 'FIXMEva_list',
136     'libvlc_callback_t': 'FIXMEcallback',
137     'libvlc_time_t': 'ctypes.c_longlong',
138     }
139
140 # Defined python classes, i.e. classes for which we want to generate
141 # class wrappers around libvlc functions
142 defined_classes=(
143     'MediaPlayer',
144     'Instance',
145     'Media',
146     'Log',
147     'LogIterator',
148     #'LogMessage',
149     'EventType',
150     'EventManager',
151     'MediaDiscoverer',
152     'MediaLibrary',
153     'MediaList',
154     'MediaListPlayer',
155     'MediaListView',
156     'TrackDescription',
157     'AudioOutput',
158     'MediaControl',
159     #'RGBPicture',
160     #'MediaControlPosition',
161     #'MediaControlStreamInformation',
162     )
163
164 # Definition of prefixes that we can strip from method names when
165 # wrapping them into class methods
166 prefixes=dict( (v, k[:-2]) for (k, v) in typ2class.iteritems() if  v in defined_classes )
167 prefixes['MediaControl']='mediacontrol_'
168
169 def parse_param(s):
170     """Parse a C parameter expression.
171
172     It is used to parse both the type/name for methods, and type/name
173     for their parameters.
174
175     It returns a tuple (type, name).
176     """
177     s=s.strip()
178     s=s.replace('const', '')
179     if 'VLC_FORWARD' in s:
180         m=forward_re.match(s)
181         s=m.group(1)+m.group(2)
182     m=param_re.search(s)
183     if m:
184         const, typ, name=m.groups()
185         while name.startswith('*'):
186             typ += '*'
187             name=name[1:]
188         if name == 'const*':
189             # K&R definition: const char * const*
190             name=''
191         typ=typ.replace(' ', '')
192         return typ, name
193     else:
194         # K&R definition: only type
195         return s.replace(' ', ''), ''
196
197 def generate_header(classes=None):
198     """Generate header code.
199     """
200     f=open('header.py', 'r')
201     for l in f:
202         if 'build_date' in l:
203             print 'build_date="%s"' % time.ctime()
204         else:
205             print l,
206     f.close()
207
208 def convert_enum_names(enums):
209     res={}
210     for (typ, name, values, comment) in enums:
211         if typ != 'enum':
212             raise Exception('This method only handles enums')
213         pyname=re.findall('(libvlc|mediacontrol)_(.+?)(_t)?$', name)[0][1]
214         if '_' in pyname:
215             pyname=pyname.title().replace('_', '')
216         else:
217             pyname=pyname.capitalize()
218         res[name]=pyname
219     return res
220
221 def generate_enums(enums):
222     for (typ, name, values, comment) in enums:
223         if typ != 'enum':
224             raise Exception('This method only handles enums')
225         pyname=typ2class[name]
226
227         print "class %s(ctypes.c_uint):" % pyname
228         print '    """%s\n    """' % comment
229
230         conv={}
231         # Convert symbol names
232         for k, v in values:
233             n=k.split('_')[-1]
234             if len(n) == 1:
235                 # Single character. Some symbols use 1_1, 5_1, etc.
236                 n="_".join( k.split('_')[:-2] )
237             if re.match('^[0-9]', n):
238                 # Cannot start an identifier with a number
239                 n='_'+n
240             conv[k]=n
241
242         for k, v in values:
243             print "    %s=%s" % (conv[k], v)
244
245         print "    _names={"
246         for k, v in values:
247             print "        %s: '%s'," % (v, conv[k])
248         print "    }"
249
250         print """
251     def __repr__(self):
252         return ".".join((self.__class__.__module__, self.__class__.__name__, self._names[self.value]))
253 """
254
255 def parse_typedef(name):
256     """Parse include file for typedef expressions.
257
258     This generates a tuple for each typedef:
259     (type, name, value_list, comment)
260     with type == 'enum' (for the moment) and value_list being a list of (name, value)
261     Note that values are string, since this is intended for code generation.
262     """
263     f=open(name, 'r')
264     accumulator=''
265     for l in f:
266         # Note: lstrip() should not be necessary, but there is 1 badly
267         # formatted comment in vlc1.0.0 includes
268         if l.lstrip().startswith('/**'):
269             comment=''
270             continue
271         elif l.startswith(' * '):
272             comment = comment + l[3:]
273             continue
274
275         l=l.strip()
276
277         if accumulator:
278             accumulator=" ".join( (accumulator, l) )
279             if l.endswith(';'):
280                 # End of definition
281                 l=accumulator
282                 accumulator=''
283         elif l.startswith('typedef enum') and not l.endswith(';'):
284             # Multiline definition. Accumulate until end of definition
285             accumulator=l
286             continue
287
288         m=enum_re.match(l)
289         if m:
290             values=[]
291             (typ, dummy, data, name)=m.groups()
292             for i, l in enumerate(paramlist_re.split(data)):
293                 l=l.strip()
294                 if l.startswith('/*'):
295                     continue
296                 if '=' in l:
297                     # A value was specified. Use it.
298                     values.append(re.split('\s*=\s*', l))
299                 else:
300                     if l:
301                         values.append( (l, str(i)) )
302             comment=comment.replace('@{', '').replace('@see', 'See').replace('\ingroup', '')
303             yield (typ, name, values, comment)
304             comment=''
305
306 def parse_include(name):
307     """Parse include file.
308
309     This generates a tuple for each function:
310     (return_type, method_name, parameter_list, comment)
311     with parameter_list being a list of tuples (parameter_type, parameter_name).
312     """
313     f=open(name, 'r')
314     accumulator=''
315     comment=''
316     for l in f:
317         # Note: lstrip() should not be necessary, but there is 1 badly
318         # formatted comment in vlc1.0.0 includes
319         if l.lstrip().startswith('/**'):
320             comment=''
321             continue
322         elif l.startswith(' * '):
323             comment = comment + l[3:]
324             continue
325
326         l=l.strip()
327
328         if accumulator:
329             accumulator=" ".join( (accumulator, l) )
330             if l.endswith(');'):
331                 # End of definition
332                 l=accumulator
333                 accumulator=''
334         elif l.startswith('VLC_PUBLIC_API') and not l.endswith(');'):
335             # Multiline definition. Accumulate until end of definition
336             accumulator=l
337             continue
338
339         m=api_re.match(l)
340         if m:
341             (ret, param)=m.groups()
342
343             rtype, method=parse_param(ret)
344
345             params=[]
346             for p in paramlist_re.split(param):
347                 params.append( parse_param(p) )
348
349             if len(params) == 1 and params[0][0] == 'void':
350                 # Empty parameter list
351                 params=[]
352
353             if list(p for p in params if not p[1]):
354                 # Empty parameter names. Have to poke into comment.
355                 names=comment_re.findall(comment)
356                 if len(names) < len(params):
357                     # Bad description: all parameters are not specified.
358                     # Generate default parameter names
359                     badnames=[ "param%d" % i for i in xrange(len(params)) ]
360                     # Put in the existing ones
361                     for (i, p) in enumerate(names):
362                         badnames[i]=names[i]
363                     names=badnames
364                     print "### Error ###"
365                     print "### Cannot get parameter names from comment for %s: %s" % (method, comment.replace("\n", ' '))
366                     # Note: this was previously
367                     # raise Exception("Cannot get parameter names from comment for %s: %s" % (method, comment))
368                     # but it prevented code generation for a minor detail (some bad descriptions).
369                 params=[ (p[0], names[i]) for (i, p) in enumerate(params) ]
370
371             for typ, name in params:
372                 if not typ in typ2class:
373                     raise Exception("No conversion for %s (from %s:%s)" % (typ, method, name))
374
375             # Transform Doxygen syntax into epydoc syntax
376             comment=comment.replace('\\param', '@param').replace('\\return', '@return')
377
378             if debug:
379                 print '********************'
380                 print l
381                 print '-------->'
382                 print "%s (%s)" % (method, rtype)
383                 for typ, name in params:
384                     print "        %s (%s)" % (name, typ)
385                 print '********************'
386             yield (rtype,
387                    method,
388                    params,
389                    comment)
390             comment=''
391
392 def output_ctypes(rtype, method, params, comment):
393     """Output ctypes decorator for the given method.
394     """
395     if method in blacklist:
396         # FIXME
397         return
398
399     if params:
400         print "prototype=ctypes.CFUNCTYPE(%s, %s)" % (typ2class.get(rtype, 'FIXME_%s' % rtype),
401                                                       ",".join( typ2class[p[0]] for p in params ))
402     else:
403         print "prototype=ctypes.CFUNCTYPE(%s)" % typ2class.get(rtype, 'FIXME_%s' % rtype)
404
405
406     if not params:
407         flags='paramflags= tuple()'
408     elif len(params) == 1:
409         flags="paramflags=( (%d, ), )" % parameter_passing[params[0][0]]
410     else:
411         flags="paramflags=%s" % ",".join( '(%d,)' % parameter_passing[p[0]] for p in params )
412     print flags
413     print '%s = prototype( ("%s", dll), paramflags )' % (method, method)
414     if '3' in flags:
415         # A VLCException is present. Process it.
416         print "%s.errcheck = check_vlc_exception" % method
417     print '%s.__doc__ = """%s"""' % (method, comment)
418     print
419
420 def parse_override(name):
421     """Parse override definitions file.
422
423     It is possible to override methods definitions in classes.
424     
425     It returns a tuple
426     (code, overriden_methods, docstring)
427     """
428     code={}
429
430     data=[]
431     current=None
432     f=open(name, 'r')
433     for l in f:
434         m=re.match('class (\S+):', l)
435         if m:
436             # Dump old data
437             if current is not None:
438                 code[current]="".join(data)
439             current=m.group(1)
440             data=[]
441             continue
442         data.append(l)
443     code[current]="".join(data)
444     f.close()
445
446     docstring={}
447     for k, v in code.iteritems():
448         if v.lstrip().startswith('"""'):
449             # Starting comment. Use it as docstring.
450             dummy, docstring[k], code[k]=v.split('"""', 2)
451
452     # Not robust wrt. internal methods, but this works for the moment.
453     overridden_methods=dict( (k, re.findall('^\s+def\s+(\w+)', v, re.MULTILINE)) for (k, v) in code.iteritems() )
454
455     return code, overridden_methods, docstring
456
457 def fix_python_comment(c):
458     """Fix comment by removing first and last parameters (self and exception)
459     """
460     data=c.replace('@{', '').replace('@see', 'See').splitlines()
461     body=itertools.takewhile(lambda l: not '@param' in l and not '@return' in l, data)
462     param=[ python_param_re.sub('\\1:\\2', l) for l in  itertools.ifilter(lambda l: '@param' in l, data) ]
463     ret=[ l.replace('@return', '@return:') for l in itertools.ifilter(lambda l: '@return' in l, data) ]
464
465     if len(param) >= 2:
466         param=param[1:-1]
467     elif len(param) == 1:
468         param=[]
469
470     return "\n".join(itertools.chain(body, param, ret))
471
472 def generate_wrappers(methods):
473     """Generate class wrappers for all appropriate methods.
474
475     @return: the set of wrapped method names
476     """
477     ret=set()
478     # Sort methods against the element they apply to.
479     elements=sorted( ( (typ2class.get(params[0][0]), rt, met, params, c)
480                        for (rt, met, params, c) in methods
481                        if params and typ2class.get(params[0][0], '_') in defined_classes
482                        ),
483                      key=operator.itemgetter(0))
484
485     overrides, overriden_methods, docstring=parse_override('override.py')
486
487     for classname, el in itertools.groupby(elements, key=operator.itemgetter(0)):
488         print """class %(name)s(object):""" % {'name': classname}
489         if classname in docstring:
490             print '    """%s\n    """' % docstring[classname]
491
492         print """
493     def __new__(cls, pointer=None):
494         '''Internal method used for instanciating wrappers from ctypes.
495         '''
496         if pointer is None:
497             raise Exception("Internal method. You should instanciate objects through other class methods (probably named 'new' or ending with 'new')")
498         if pointer == 0:
499             return None
500         else:
501             o=object.__new__(cls)
502             o._as_parameter_=ctypes.c_void_p(pointer)
503             return o
504
505     @staticmethod
506     def from_param(arg):
507         '''(INTERNAL) ctypes parameter conversion method.
508         '''
509         return arg._as_parameter_
510 """ % {'name': classname}
511
512         if classname in overrides:
513             print overrides[classname]
514
515         prefix=prefixes.get(classname, '')
516
517         for cl, rtype, method, params, comment in el:
518             if method in blacklist:
519                 continue
520             # Strip prefix
521             name=method.replace(prefix, '').replace('libvlc_', '')
522             ret.add(method)
523             if name in overriden_methods.get(cl, []):
524                 # Method already defined in override.py
525                 continue
526
527             if params:
528                 params[0]=(params[0][0], 'self')
529             if params and params[-1][0] in ('libvlc_exception_t*', 'mediacontrol_Exception*'):
530                 args=", ".join( p[1] for p in params[:-1] )
531             else:
532                 args=", ".join( p[1] for p in params )
533
534             print "    def %s(%s):" % (name, args)
535             print '        """%s\n        """' % fix_python_comment(comment)
536             if params and params[-1][0] == 'libvlc_exception_t*':
537                 # Exception handling
538                 print "        e=VLCException()"
539                 print "        return %s(%s, e)" % (method, args)
540             elif params and params[-1][0] == 'mediacontrol_Exception*':
541                 # Exception handling
542                 print "        e=MediaControlException()"
543                 print "        return %s(%s, e)" % (method, args)
544             else:
545                 print "        return %s(%s)" % (method, args)
546             print
547
548             # Check for standard methods
549             if name == 'count':
550                 # There is a count method. Generate a __len__ one.
551                 print "    def __len__(self):"
552                 print "        e=VLCException()"
553                 print "        return %s(self, e)" % method
554                 print
555             elif name.endswith('item_at_index'):
556                 # Indexable (and thus iterable)"
557                 print "    def __getitem__(self, i):"
558                 print "        e=VLCException()"
559                 print "        return %s(self, i, e)" % method
560                 print
561                 print "    def __iter__(self):"
562                 print "        e=VLCException()"
563                 print "        for i in xrange(len(self)):"
564                 print "            yield self[i]"
565                 print
566
567     return ret
568
569 if __name__ == '__main__':
570     enums=[]
571     for name in sys.argv[1:]:
572         enums.extend(list(parse_typedef(name)))
573     # Generate python names for enums
574     typ2class.update(convert_enum_names(enums))
575
576     methods=[]
577     for name in sys.argv[1:]:
578         methods.extend(list(parse_include(name)))
579     if debug:
580         sys.exit(0)
581
582     generate_header()
583     generate_enums(enums)
584     wrapped=generate_wrappers(methods)
585     for l in methods:
586         output_ctypes(*l)
587
588     all=set( t[1] for t in methods )
589     not_wrapped=all.difference(wrapped)
590     print "# Not wrapped methods:"
591     for m in not_wrapped:
592         print "#   ", m
593