]> git.sesse.net Git - vlc/blob - bindings/python-ctypes/generate.py
pyton-ctypes: reset comment when parsing includes
[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]="\n".join(data)
433             current=m.group(1)
434             data=[]
435             continue
436         data.append(l)
437     res[current]="\n".join(data)
438     f.close()
439     return res
440
441 def fix_python_comment(c):
442     """Fix comment by removing first and last parameters (self and exception)
443     """
444     data=c.splitlines()
445     body=itertools.takewhile(lambda l: not '@param' in l, data)
446     param=[ python_param_re.sub('\\1:\\2', l) for l in  itertools.ifilter(lambda l: '@param' in l, data) ]
447     ret=[ l.replace('@return', '@return:') for l in itertools.ifilter(lambda l: '@return' in l, data) ]
448
449     if len(param) >= 2:
450         param=param[1:-1]
451     elif len(param) == 1:
452         param=[]
453
454     return "\n".join(itertools.chain(body, param, ret))
455
456 def generate_wrappers(methods):
457     """Generate class wrappers for all appropriate methods.
458
459     @return: the set of wrapped method names
460     """
461     ret=set()
462     # Sort methods against the element they apply to.
463     elements=sorted( ( (typ2class.get(params[0][0]), rt, met, params, c)
464                        for (rt, met, params, c) in methods
465                        if params and typ2class.get(params[0][0], '_') in defined_classes
466                        ),
467                      key=operator.itemgetter(0))
468
469     overrides=parse_override('override.py')
470
471     for classname, el in itertools.groupby(elements, key=operator.itemgetter(0)):
472         print """
473 class %(name)s(object):
474     def __new__(cls, pointer=None):
475         '''Internal method used for instanciating wrappers from ctypes.
476         '''
477         if pointer is None:
478             raise Exception("Internal method. You should instanciate objects through other class methods (probably named 'new' or ending with 'new')")
479         if pointer == 0:
480             return None
481         else:
482             o=object.__new__(cls)
483             o._as_parameter_=ctypes.c_void_p(pointer)
484             return o
485
486     @staticmethod
487     def from_param(arg):
488         '''(INTERNAL) ctypes parameter conversion method.
489         '''
490         return arg._as_parameter_
491 """ % {'name': classname}
492
493         if classname in overrides:
494             print overrides[classname]
495
496         prefix=prefixes.get(classname, '')
497
498         for cl, rtype, method, params, comment in el:
499             if method in blacklist:
500                 continue
501             # Strip prefix
502             name=method.replace(prefix, '').replace('libvlc_', '')
503             ret.add(method)
504             if params:
505                 params[0]=(params[0][0], 'self')
506             if params and params[-1][0] in ('libvlc_exception_t*', 'mediacontrol_Exception*'):
507                 args=", ".join( p[1] for p in params[:-1] )
508             else:
509                 args=", ".join( p[1] for p in params )
510
511             print "    def %s(%s):" % (name, args)
512             print '        """%s\n"""' % fix_python_comment(comment)
513             if params and params[-1][0] == 'libvlc_exception_t*':
514                 # Exception handling
515                 print "        e=VLCException()"
516                 print "        return %s(%s, e)" % (method, args)
517             elif params and params[-1][0] == 'mediacontrol_Exception*':
518                 # Exception handling
519                 print "        e=MediaControlException()"
520                 print "        return %s(%s, e)" % (method, args)
521             else:
522                 print "        return %s(%s)" % (method, args)
523             print
524
525             # Check for standard methods
526             if name == 'count':
527                 # There is a count method. Generate a __len__ one.
528                 print "    def __len__(self):"
529                 print "        e=VLCException()"
530                 print "        return %s(self, e)" % method
531                 print
532             elif name.endswith('item_at_index'):
533                 # Indexable (and thus iterable)"
534                 print "    def __getitem__(self, i):"
535                 print "        e=VLCException()"
536                 print "        return %s(self, i, e)" % method
537                 print
538                 print "    def __iter__(self):"
539                 print "        e=VLCException()"
540                 print "        for i in xrange(len(self)):"
541                 print "            yield self[i]"
542                 print
543
544     return ret
545
546 if __name__ == '__main__':
547     enums=[]
548     for name in sys.argv[1:]:
549         enums.extend(list(parse_typedef(name)))
550     # Generate python names for enums
551     typ2class.update(convert_enum_names(enums))
552
553     methods=[]
554     for name in sys.argv[1:]:
555         methods.extend(list(parse_include(name)))
556     if debug:
557         sys.exit(0)
558
559     generate_header()
560     generate_enums(enums)
561     wrapped=generate_wrappers(methods)
562     for l in methods:
563         output_ctypes(*l)
564
565     all=set( t[1] for t in methods )
566     not_wrapped=all.difference(wrapped)
567     print "# Not wrapped methods:"
568     for m in not_wrapped:
569         print "#   ", m
570