1#!/usr/bin/python
2# -*- Mode: Python -*-
3# vi:si:et:sw=4:sts=4:ts=4
4
5"""
6parse, merge and write gstdoc-scanobj files
7"""
8
9from __future__ import print_function, unicode_literals
10
11import codecs
12import os
13import sys
14
15def debug(*args):
16    pass
17
18# OrderedDict class based on
19# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/107747
20# Licensed under the Python License
21class OrderedDict(dict):
22    def __init__(self):
23        self._keys = []
24        dict.__init__(self)
25
26    def __delitem__(self, key):
27        dict.__delitem__(self, key)
28        self._keys.remove(key)
29
30    def __setitem__(self, key, item):
31        dict.__setitem__(self, key, item)
32        if key not in self._keys: self._keys.append(key)
33
34    def clear(self):
35        dict.clear(self)
36        self._keys = []
37
38    def copy(self):
39        dict = dict.copy(self)
40        dict._keys = self._keys[:]
41        return dict
42
43    def items(self):
44        return zip(self._keys, self.values())
45
46    def keys(self):
47        return self._keys
48
49    def popitem(self):
50        try:
51            key = self._keys[-1]
52        except IndexError:
53            raise KeyError('dictionary is empty')
54
55        val = self[key]
56        del self[key]
57
58        return (key, val)
59
60    def setdefault(self, key, failobj = None):
61        dict.setdefault(self, key, failobj)
62        if key not in self._keys: self._keys.append(key)
63
64    def update(self, dict):
65        dict.update(self, dict)
66        for key in dict.keys():
67            if key not in self._keys: self._keys.append(key)
68
69    def values(self):
70        return map(self.get, self._keys)
71
72class Object:
73    def __init__(self, name):
74        self._signals = OrderedDict()
75        self._args = OrderedDict()
76        self.name = name
77
78    def __repr__(self):
79        return "<Object %s>" % self.name
80
81    def add_signal(self, signal, overwrite=True):
82        if not overwrite and signal.name in self._signals:
83            raise IndexError("signal %s already in %r" % (signal.name, self))
84        self._signals[signal.name] = signal
85
86    def add_arg(self, arg, overwrite=True):
87        if not overwrite and arg.name in self._args:
88            raise IndexError("arg %s already in %r" % (arg.name, self))
89        self._args[arg.name] = arg
90
91class Docable:
92    def __init__(self, **kwargs):
93        for key in self.attrs:
94            setattr(self, key, kwargs[key])
95        self.dict = kwargs
96
97    def __repr__(self):
98        return "<%r %s>" % (str(self.__class__), self.name)
99
100class Signal(Docable):
101    attrs = ['name', 'returns', 'args']
102
103class Arg(Docable):
104    attrs = ['name', 'type', 'range', 'flags', 'nick', 'blurb', 'default']
105
106class GDoc:
107    def load_file(self, filename):
108        try:
109            lines = codecs.open(filename, encoding='utf-8').readlines()
110            self.load_data("".join(lines))
111        except IOError:
112            print ("WARNING - could not read from %s" % filename)
113        except UnicodeDecodeError as e:
114            print ("WARNING - could not parse %s: %s" % (filename, e))
115
116    def save_file(self, filename, backup=False):
117        """
118        Save the information to the given file if the file content changed.
119        """
120        olddata = None
121        try:
122            lines = codecs.open(filename, encoding='utf-8').readlines()
123            olddata = "".join(lines)
124        except IOError:
125            print ("WARNING - could not read from %s" % filename)
126        newdata = self.get_data()
127        if olddata and olddata == newdata:
128            return
129
130        if olddata:
131            if backup:
132                os.rename(filename, filename + '.bak')
133
134        handle = codecs.open(filename, "w", encoding='utf-8')
135        handle.write(newdata)
136        handle.close()
137
138class Signals(GDoc):
139    def __init__(self):
140        self._objects = OrderedDict()
141
142    def load_data(self, data):
143        """
144        Load the .signals lines, creating our list of objects and signals.
145        """
146        import re
147        smatcher = re.compile(
148            '(?s)'                                      # make . match \n
149            '<SIGNAL>\n(.*?)</SIGNAL>\n'
150            )
151        nmatcher = re.compile(
152            '<NAME>'
153            '(?P<object>\S*)'                           # store object
154            '::'
155            '(?P<signal>\S*)'                           # store signal
156            '</NAME>'
157        )
158        rmatcher = re.compile(
159            '(?s)'                                      # make . match \n
160            '<RETURNS>(?P<returns>\S*)</RETURNS>\n'     # store returns
161            '(?P<args>.*)'                              # store args
162        )
163        for block in smatcher.findall(data):
164            nmatch = nmatcher.search(block)
165            if nmatch:
166                o = nmatch.group('object')
167                debug("Found object", o)
168                debug("Found signal", nmatch.group('signal'))
169                if o not in self._objects:
170                    object = Object(o)
171                    self._objects[o] = object
172
173                rmatch = rmatcher.search(block)
174                if rmatch:
175                    dict = rmatch.groupdict().copy()
176                    dict['name'] = nmatch.group('signal')
177                    signal = Signal(**dict)
178                    self._objects[o].add_signal(signal)
179
180    def get_data(self):
181        lines = []
182        for o in self._objects.values():
183            for s in o._signals.values():
184                block = """<SIGNAL>
185<NAME>%(object)s::%(name)s</NAME>
186<RETURNS>%(returns)s</RETURNS>
187%(args)s</SIGNAL>
188"""
189                d = s.dict.copy()
190                d['object'] = o.name
191                lines.append(block % d)
192
193        return "\n".join(lines) + '\n'
194
195class Args(GDoc):
196    def __init__(self):
197        self._objects = OrderedDict()
198
199    def load_data(self, data):
200        """
201        Load the .args lines, creating our list of objects and args.
202        """
203        import re
204        amatcher = re.compile(
205            '(?s)'                                      # make . match \n
206            '<ARG>\n(.*?)</ARG>\n'
207            )
208        nmatcher = re.compile(
209            '<NAME>'
210            '(?P<object>\S*)'                           # store object
211            '::'
212            '(?P<arg>\S*)'                              # store arg
213            '</NAME>'
214        )
215        rmatcher = re.compile(
216            '(?s)'                                      # make . match \n
217            '<TYPE>(?P<type>\S*)</TYPE>\n'              # store type
218            '<RANGE>(?P<range>.*?)</RANGE>\n'           # store range
219            '<FLAGS>(?P<flags>\S*)</FLAGS>\n'           # store flags
220            '<NICK>(?P<nick>.*?)</NICK>\n'              # store nick
221            '<BLURB>(?P<blurb>.*?)</BLURB>\n'           # store blurb
222            '<DEFAULT>(?P<default>.*?)</DEFAULT>\n'     # store default
223        )
224        for block in amatcher.findall(data):
225            nmatch = nmatcher.search(block)
226            if nmatch:
227                o = nmatch.group('object')
228                debug("Found object", o)
229                debug("Found arg", nmatch.group('arg'))
230                if o not in self._objects:
231                    object = Object(o)
232                    self._objects[o] = object
233
234                rmatch = rmatcher.search(block)
235                if rmatch:
236                    dict = rmatch.groupdict().copy()
237                    dict['name'] = nmatch.group('arg')
238                    arg = Arg(**dict)
239                    self._objects[o].add_arg(arg)
240                else:
241                    print ("ERROR: could not match arg from block %s" % block)
242
243    def get_data(self):
244        lines = []
245        for o in self._objects.values():
246            for a in o._args.values():
247                block = """<ARG>
248<NAME>%(object)s::%(name)s</NAME>
249<TYPE>%(type)s</TYPE>
250<RANGE>%(range)s</RANGE>
251<FLAGS>%(flags)s</FLAGS>
252<NICK>%(nick)s</NICK>
253<BLURB>%(blurb)s</BLURB>
254<DEFAULT>%(default)s</DEFAULT>
255</ARG>
256"""
257                d = a.dict.copy()
258                d['object'] = o.name
259                lines.append(block % d)
260
261        return "\n".join(lines) + '\n'
262
263class SingleLine(GDoc):
264    def __init__(self):
265        self._objects = []
266
267    def load_data(self, data):
268        """
269        Load the .interfaces/.prerequisites lines, merge duplicates
270        """
271        # split data on '\n'
272        lines = data.splitlines();
273        # merge them into self._objects
274        for line in lines:
275            if line not in self._objects:
276                self._objects.append(line)
277
278    def get_data(self):
279        lines = sorted(self._objects)
280        return "\n".join(lines) + '\n'
281
282def main(argv):
283    modulename = None
284    try:
285        modulename = argv[1]
286    except IndexError:
287        sys.stderr.write('Please provide a documentation module name\n')
288        sys.exit(1)
289
290    signals = Signals()
291    signals.load_file(modulename + '.signals')
292    signals.load_file(modulename + '.signals.new')
293    signals.save_file(modulename + '.signals', backup=True)
294    os.unlink(modulename + '.signals.new')
295
296    args = Args()
297    args.load_file(modulename + '.args')
298    args.load_file(modulename + '.args.new')
299    args.save_file(modulename + '.args', backup=True)
300    os.unlink(modulename + '.args.new')
301
302    ifaces = SingleLine()
303    ifaces.load_file(modulename + '.interfaces')
304    ifaces.load_file(modulename + '.interfaces.new')
305    ifaces.save_file(modulename + '.interfaces', backup=True)
306    os.unlink(modulename + '.interfaces.new')
307
308    prereq = SingleLine()
309    prereq.load_file(modulename + '.prerequisites')
310    prereq.load_file(modulename + '.prerequisites.new')
311    prereq.save_file(modulename + '.prerequisites', backup=True)
312    os.unlink(modulename + '.prerequisites.new')
313
314main(sys.argv)
315