]> git.sesse.net Git - vlc/blob - bindings/python-ctypes/generate.py
python-ctypes: do not redefine overridden methods
[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) 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) 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
229         conv={}
230         # Convert symbol names
231         for k, v in values:
232             n=k.split('_')[-1]
233             if len(n) == 1:
234                 # Single character. Some symbols use 1_1, 5_1, etc.
235                 n="_".join( k.split('_')[:-2] )
236             if re.match('^[0-9]', n):
237                 # Cannot start an identifier with a number
238                 n='_'+n
239             conv[k]=n
240
241         for k, v in values:
242             print "    %s=%s" % (conv[k], v)
243
244         print "    _names={"
245         for k, v in values:
246             print "        %s: '%s'," % (v, conv[k])
247         print "    }"
248
249         print """
250     def __repr__(self):
251         return ".".join((self.__class__.__module__, self.__class__.__name__, self._names[self.value]))
252 """
253
254 def parse_typedef(name):
255     """Parse include file for typedef expressions.
256
257     This generates a tuple for each typedef:
258     (type, name, value_list)
259     with type == 'enum' (for the moment) and value_list being a list of (name, value)
260     Note that values are string, since this is intended for code generation.
261     """
262     f=open(name, 'r')
263     accumulator=''
264     for l in f:
265         # Note: lstrip() should not be necessary, but there is 1 badly
266         # formatted comment in vlc1.0.0 includes
267         if l.lstrip().startswith('/**'):
268             comment=''
269             continue
270         elif l.startswith(' * '):
271             comment = comment + l[3:]
272             continue
273
274         l=l.strip()
275
276         if accumulator:
277             accumulator=" ".join( (accumulator, l) )
278             if l.endswith(';'):
279                 # End of definition
280                 l=accumulator
281                 accumulator=''
282         elif l.startswith('typedef enum') and not l.endswith(';'):
283             # Multiline definition. Accumulate until end of definition
284             accumulator=l
285             continue
286
287         m=enum_re.match(l)
288         if m:
289             values=[]
290             (typ, dummy, data, name)=m.groups()
291             for i, l in enumerate(paramlist_re.split(data)):
292                 l=l.strip()
293                 if l.startswith('/*'):
294                     continue
295                 if '=' in l:
296                     # A value was specified. Use it.
297                     values.append(re.split('\s*=\s*', l))
298                 else:
299                     if l:
300                         values.append( (l, str(i)) )
301             yield (typ, name, values)
302
303 def parse_include(name):
304     """Parse include file.
305
306     This generates a tuple for each function:
307     (return_type, method_name, parameter_list, comment)
308     with parameter_list being a list of tuples (parameter_type, parameter_name).
309     """
310     f=open(name, 'r')
311     accumulator=''
312     comment=''
313     for l in f:
314         # Note: lstrip() should not be necessary, but there is 1 badly
315         # formatted comment in vlc1.0.0 includes
316         if l.lstrip().startswith('/**'):
317             comment=''
318             continue
319         elif l.startswith(' * '):
320             comment = comment + l[3:]
321             continue
322
323         l=l.strip()
324
325         if accumulator:
326             accumulator=" ".join( (accumulator, l) )
327             if l.endswith(');'):
328                 # End of definition
329                 l=accumulator
330                 accumulator=''
331         elif l.startswith('VLC_PUBLIC_API') and not l.endswith(');'):
332             # Multiline definition. Accumulate until end of definition
333             accumulator=l
334             continue
335
336         m=api_re.match(l)
337         if m:
338             (ret, param)=m.groups()
339
340             rtype, method=parse_param(ret)
341
342             params=[]
343             for p in paramlist_re.split(param):
344                 params.append( parse_param(p) )
345
346             if len(params) == 1 and params[0][0] == 'void':
347                 # Empty parameter list
348                 params=[]
349
350             if list(p for p in params if not p[1]):
351                 # Empty parameter names. Have to poke into comment.
352                 names=comment_re.findall(comment)
353                 if len(names) < len(params):
354                     # Bad description: all parameters are not specified.
355                     # Generate default parameter names
356                     badnames=[ "param%d" % i for i in xrange(len(params)) ]
357                     # Put in the existing ones
358                     for (i, p) in enumerate(names):
359                         badnames[i]=names[i]
360                     names=badnames
361                     print "### Error ###"
362                     print "### Cannot get parameter names from comment for %s: %s" % (method, comment.replace("\n", ' '))
363                     # Note: this was previously
364                     # raise Exception("Cannot get parameter names from comment for %s: %s" % (method, comment))
365                     # but it prevented code generation for a minor detail (some bad descriptions).
366                 params=[ (p[0], names[i]) for (i, p) in enumerate(params) ]
367
368             for typ, name in params:
369                 if not typ in typ2class:
370                     raise Exception("No conversion for %s (from %s:%s)" % (typ, method, name))
371
372             # Transform Doxygen syntax into epydoc syntax
373             comment=comment.replace('\\param', '@param').replace('\\return', '@return')
374
375             if debug:
376                 print '********************'
377                 print l
378                 print '-------->'
379                 print "%s (%s)" % (method, rtype)
380                 for typ, name in params:
381                     print "        %s (%s)" % (name, typ)
382                 print '********************'
383             yield (rtype,
384                    method,
385                    params,
386                    comment)
387             comment=''
388
389 def output_ctypes(rtype, method, params, comment):
390     """Output ctypes decorator for the given method.
391     """
392     if method in blacklist:
393         # FIXME
394         return
395
396     if params:
397         print "prototype=ctypes.CFUNCTYPE(%s, %s)" % (typ2class.get(rtype, 'FIXME_%s' % rtype),
398                                                       ",".join( typ2class[p[0]] for p in params ))
399     else:
400         print "prototype=ctypes.CFUNCTYPE(%s)" % typ2class.get(rtype, 'FIXME_%s' % rtype)
401
402
403     if not params:
404         flags='paramflags= tuple()'
405     elif len(params) == 1:
406         flags="paramflags=( (%d, ), )" % parameter_passing[params[0][0]]
407     else:
408         flags="paramflags=%s" % ",".join( '(%d,)' % parameter_passing[p[0]] for p in params )
409     print flags
410     print '%s = prototype( ("%s", dll), paramflags )' % (method, method)
411     if '3' in flags:
412         # A VLCException is present. Process it.
413         print "%s.errcheck = check_vlc_exception" % method
414     print '%s.__doc__ = """%s"""' % (method, comment)
415     print
416
417 def parse_override(name):
418     """Parse override definitions file.
419
420     It is possible to override methods definitions in classes.
421     """
422     res={}
423
424     data=[]
425     current=None
426     f=open(name, 'r')
427     for l in f:
428         m=re.match('class (\S+):', l)
429         if m:
430             # Dump old data
431             if current is not None:
432                 res[current]="".join(data)
433             current=m.group(1)
434             data=[]
435             continue
436         data.append(l)
437     res[current]="".join(data)
438     f.close()
439     
440     # Not robust wrt. internal methods, but this works for the moment.
441     overriden_methods=dict( (k, re.findall('^\s+def\s+(\w+)', v)) for (k, v) in res.iteritems() )
442
443     return res, overriden_methods
444
445 def fix_python_comment(c):
446     """Fix comment by removing first and last parameters (self and exception)
447     """
448     data=c.splitlines()
449     body=itertools.takewhile(lambda l: not '@param' in l, data)
450     param=[ python_param_re.sub('\\1:\\2', l) for l in  itertools.ifilter(lambda l: '@param' in l, data) ]
451     ret=[ l.replace('@return', '@return:') for l in itertools.ifilter(lambda l: '@return' in l, data) ]
452
453     if len(param) >= 2:
454         param=param[1:-1]
455     elif len(param) == 1:
456         param=[]
457
458     return "\n".join(itertools.chain(body, param, ret))
459
460 def generate_wrappers(methods):
461     """Generate class wrappers for all appropriate methods.
462
463     @return: the set of wrapped method names
464     """
465     ret=set()
466     # Sort methods against the element they apply to.
467     elements=sorted( ( (typ2class.get(params[0][0]), rt, met, params, c)
468                        for (rt, met, params, c) in methods
469                        if params and typ2class.get(params[0][0], '_') in defined_classes
470                        ),
471                      key=operator.itemgetter(0))
472
473     overrides, overriden_methods=parse_override('override.py')
474
475     for classname, el in itertools.groupby(elements, key=operator.itemgetter(0)):
476         print """
477 class %(name)s(object):
478     def __new__(cls, pointer=None):
479         '''Internal method used for instanciating wrappers from ctypes.
480         '''
481         if pointer is None:
482             raise Exception("Internal method. You should instanciate objects through other class methods (probably named 'new' or ending with 'new')")
483         if pointer == 0:
484             return None
485         else:
486             o=object.__new__(cls)
487             o._as_parameter_=ctypes.c_void_p(pointer)
488             return o
489
490     @staticmethod
491     def from_param(arg):
492         '''(INTERNAL) ctypes parameter conversion method.
493         '''
494         return arg._as_parameter_
495 """ % {'name': classname}
496
497         if classname in overrides:
498             print overrides[classname]
499
500         prefix=prefixes.get(classname, '')
501
502         for cl, rtype, method, params, comment in el:
503             if method in blacklist:
504                 continue
505             # Strip prefix
506             name=method.replace(prefix, '').replace('libvlc_', '')
507             ret.add(method)
508             if name in overriden_methods.get(cl, []):
509                 # Method already defined in override.py
510                 continue
511
512             if params:
513                 params[0]=(params[0][0], 'self')
514             if params and params[-1][0] in ('libvlc_exception_t*', 'mediacontrol_Exception*'):
515                 args=", ".join( p[1] for p in params[:-1] )
516             else:
517                 args=", ".join( p[1] for p in params )
518
519             print "    def %s(%s):" % (name, args)
520             print '        """%s\n"""' % fix_python_comment(comment)
521             if params and params[-1][0] == 'libvlc_exception_t*':
522                 # Exception handling
523                 print "        e=VLCException()"
524                 print "        return %s(%s, e)" % (method, args)
525             elif params and params[-1][0] == 'mediacontrol_Exception*':
526                 # Exception handling
527                 print "        e=MediaControlException()"
528                 print "        return %s(%s, e)" % (method, args)
529             else:
530                 print "        return %s(%s)" % (method, args)
531             print
532
533             # Check for standard methods
534             if name == 'count':
535                 # There is a count method. Generate a __len__ one.
536                 print "    def __len__(self):"
537                 print "        e=VLCException()"
538                 print "        return %s(self, e)" % method
539                 print
540             elif name.endswith('item_at_index'):
541                 # Indexable (and thus iterable)"
542                 print "    def __getitem__(self, i):"
543                 print "        e=VLCException()"
544                 print "        return %s(self, i, e)" % method
545                 print
546                 print "    def __iter__(self):"
547                 print "        e=VLCException()"
548                 print "        for i in xrange(len(self)):"
549                 print "            yield self[i]"
550                 print
551
552     return ret
553
554 if __name__ == '__main__':
555     enums=[]
556     for name in sys.argv[1:]:
557         enums.extend(list(parse_typedef(name)))
558     # Generate python names for enums
559     typ2class.update(convert_enum_names(enums))
560
561     methods=[]
562     for name in sys.argv[1:]:
563         methods.extend(list(parse_include(name)))
564     if debug:
565         sys.exit(0)
566
567     generate_header()
568     generate_enums(enums)
569     wrapped=generate_wrappers(methods)
570     for l in methods:
571         output_ctypes(*l)
572
573     all=set( t[1] for t in methods )
574     not_wrapped=all.difference(wrapped)
575     print "# Not wrapped methods:"
576     for m in not_wrapped:
577         print "#   ", m
578