2085
|
1 #!/usr/bin/env python2 |
|
2 #-*- coding: utf-8 -*- |
|
3 |
|
4 # SàT: a XMPP client |
|
5 # Copyright (C) 2009-2016 Jérôme Poisson (goffi@goffi.org) |
|
6 |
|
7 # This program is free software: you can redistribute it and/or modify |
|
8 # it under the terms of the GNU Affero General Public License as published by |
|
9 # the Free Software Foundation, either version 3 of the License, or |
|
10 # (at your option) any later version. |
|
11 |
|
12 # This program is distributed in the hope that it will be useful, |
|
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
15 # GNU Affero General Public License for more details. |
|
16 |
|
17 # You should have received a copy of the GNU Affero General Public License |
|
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
19 |
|
20 """base constructor class""" |
|
21 |
|
22 from sat.bridge.bridge_constructor.constants import Const as C |
|
23 from ConfigParser import NoOptionError |
|
24 import sys |
|
25 import os |
|
26 import os.path |
|
27 import re |
|
28 from importlib import import_module |
|
29 |
|
30 |
|
31 class ParseError(Exception): |
|
32 #Used when the signature parsing is going wrong (invalid signature ?) |
|
33 pass |
|
34 |
|
35 |
|
36 class Constructor(object): |
|
37 NAME = None # used in arguments parsing, filename will be used if not set |
|
38 # following attribute are used by default generation method |
|
39 # they can be set to dict of strings using python formatting syntax |
|
40 # dict keys will be used to select part to replace (e.g. "signals" key will |
|
41 # replace ##SIGNALS_PART## in template), while the value is the format |
|
42 # keys starting with "signal" will be used for signals, while ones starting with |
|
43 # "method" will be used for methods |
|
44 # check D-Bus constructor for an example |
|
45 CORE_FORMATS = None |
|
46 CORE_TEMPLATE = None |
|
47 CORE_DEST = None |
|
48 FRONTEND_FORMATS = None |
|
49 FRONTEND_TEMPLATE = None |
|
50 FRONTEND_DEST = None |
|
51 |
|
52 def __init__(self, bridge_template, options): |
|
53 self.bridge_template = bridge_template |
|
54 self.args = options |
|
55 |
|
56 @property |
|
57 def constructor_dir(self): |
|
58 constructor_mod = import_module(self.__module__) |
|
59 return os.path.dirname(constructor_mod.__file__) |
|
60 |
|
61 def getValues(self, name): |
|
62 """Return values of a function in a dict |
|
63 @param name: Name of the function to get |
|
64 @return: dict, each key has the config value or None if the value is not set""" |
|
65 function = {} |
|
66 for option in ['type', 'category', 'sig_in', 'sig_out', 'doc']: |
|
67 try: |
|
68 value = self.bridge_template.get(name, option) |
|
69 except NoOptionError: |
|
70 value = None |
|
71 function[option] = value |
|
72 return function |
|
73 |
|
74 def getDefault(self, name): |
|
75 """Return default values of a function in a dict |
|
76 @param name: Name of the function to get |
|
77 @return: dict, each key is the integer param number (no key if no default value)""" |
|
78 default_dict = {} |
|
79 def_re = re.compile(r"param_(\d+)_default") |
|
80 |
|
81 for option in self.bridge_template.options(name): |
|
82 match = def_re.match(option) |
|
83 if match: |
|
84 try: |
|
85 idx = int(match.group(1)) |
|
86 except ValueError: |
|
87 raise ParseError("Invalid value [%s] for parameter number" % match.group(1)) |
|
88 default_dict[idx] = self.bridge_template.get(name, option) |
|
89 |
|
90 return default_dict |
|
91 |
|
92 def getFlags(self, name): |
|
93 """Return list of flags set for this function |
|
94 |
|
95 @param name: Name of the function to get |
|
96 @return: List of flags (string) |
|
97 """ |
|
98 flags = [] |
|
99 for option in self.bridge_template.options(name): |
|
100 if option in C.DECLARATION_FLAGS: |
|
101 flags.append(option) |
|
102 return flags |
|
103 |
|
104 def getArgumentsDoc(self, name): |
|
105 """Return documentation of arguments |
|
106 @param name: Name of the function to get |
|
107 @return: dict, each key is the integer param number (no key if no argument doc), value is a tuple (name, doc)""" |
|
108 doc_dict = {} |
|
109 option_re = re.compile(r"doc_param_(\d+)") |
|
110 value_re = re.compile(r"^(\w+): (.*)$", re.MULTILINE | re.DOTALL) |
|
111 for option in self.bridge_template.options(name): |
|
112 if option == 'doc_return': |
|
113 doc_dict['return'] = self.bridge_template.get(name, option) |
|
114 continue |
|
115 match = option_re.match(option) |
|
116 if match: |
|
117 try: |
|
118 idx = int(match.group(1)) |
|
119 except ValueError: |
|
120 raise ParseError("Invalid value [%s] for parameter number" % match.group(1)) |
|
121 value_match = value_re.match(self.bridge_template.get(name, option)) |
|
122 if not value_match: |
|
123 raise ParseError("Invalid value for parameter doc [%i]" % idx) |
|
124 doc_dict[idx] = (value_match.group(1), value_match.group(2)) |
|
125 return doc_dict |
|
126 |
|
127 def getDoc(self, name): |
|
128 """Return documentation of the method |
|
129 @param name: Name of the function to get |
|
130 @return: string documentation, or None""" |
|
131 if self.bridge_template.has_option(name, "doc"): |
|
132 return self.bridge_template.get(name, "doc") |
|
133 return None |
|
134 |
|
135 def argumentsParser(self, signature): |
|
136 """Generator which return individual arguments signatures from a global signature""" |
|
137 start = 0 |
|
138 i = 0 |
|
139 |
|
140 while i < len(signature): |
|
141 if signature[i] not in ['b', 'y', 'n', 'i', 'x', 'q', 'u', 't', 'd', 's', 'a']: |
|
142 raise ParseError("Unmanaged attribute type [%c]" % signature[i]) |
|
143 |
|
144 if signature[i] == 'a': |
|
145 i += 1 |
|
146 if signature[i] != '{' and signature[i] != '(': # FIXME: must manage tuples out of arrays |
|
147 i += 1 |
|
148 yield signature[start:i] |
|
149 start = i |
|
150 continue # we have a simple type for the array |
|
151 opening_car = signature[i] |
|
152 assert(opening_car in ['{', '(']) |
|
153 closing_car = '}' if opening_car == '{' else ')' |
|
154 opening_count = 1 |
|
155 while (True): # we have a dict or a list of tuples |
|
156 i += 1 |
|
157 if i >= len(signature): |
|
158 raise ParseError("missing }") |
|
159 if signature[i] == opening_car: |
|
160 opening_count += 1 |
|
161 if signature[i] == closing_car: |
|
162 opening_count -= 1 |
|
163 if opening_count == 0: |
|
164 break |
|
165 i += 1 |
|
166 yield signature[start:i] |
|
167 start = i |
|
168 |
|
169 def getArguments(self, signature, name=None, default=None, unicode_protect=False): |
|
170 """Return arguments to user given a signature |
|
171 |
|
172 @param signature: signature in the short form (using s,a,i,b etc) |
|
173 @param name: dictionary of arguments name like given by getArguments |
|
174 @param default: dictionary of default values, like given by getDefault |
|
175 @param unicode_protect: activate unicode protection on strings (return strings as unicode(str)) |
|
176 @return: list of arguments that correspond to a signature (e.g.: "sss" return "arg1, arg2, arg3") |
|
177 """ |
|
178 idx = 0 |
|
179 attr_string = [] |
|
180 |
|
181 for arg in self.argumentsParser(signature): |
|
182 attr_string.append(("unicode(%(name)s)%(default)s" if (unicode_protect and arg == 's') else "%(name)s%(default)s") % { |
|
183 'name': name[idx][0] if (name and idx in name) else "arg_%i" % idx, |
|
184 'default': "=" + default[idx] if (default and idx in default) else ''}) |
|
185 # give arg_1, arg2, etc or name1, name2=default, etc. |
|
186 #give unicode(arg_1), unicode(arg_2), etc. if unicode_protect is set and arg is a string |
|
187 idx += 1 |
|
188 |
|
189 return ", ".join(attr_string) |
|
190 |
|
191 def getTemplatePath(self, template_file): |
|
192 """return template path corresponding to file name |
|
193 |
|
194 @param template_file(str): name of template file |
|
195 """ |
|
196 return os.path.join(self.constructor_dir, template_file) |
|
197 |
|
198 def core_completion_method(self, completion, function, default, arg_doc, async_): |
|
199 """override this method to extend completion""" |
|
200 pass |
|
201 |
|
202 def core_completion_signal(self, completion, function, default, arg_doc, async_): |
|
203 """override this method to extend completion""" |
|
204 pass |
|
205 |
|
206 def frontend_completion_method(self, completion, function, default, arg_doc, async_): |
|
207 """override this method to extend completion""" |
|
208 pass |
|
209 |
|
210 def frontend_completion_signal(self, completion, function, default, arg_doc, async_): |
|
211 """override this method to extend completion""" |
|
212 pass |
|
213 |
|
214 |
|
215 def generate(self, side): |
|
216 """generate bridge |
|
217 |
|
218 call generateCoreSide or generateFrontendSide if they exists |
|
219 else call generic self._generate method |
|
220 """ |
|
221 try: |
|
222 if side == "core": |
|
223 method = self.generateCoreSide |
|
224 elif side == "frontend": |
|
225 method = self.generateFrontendSide |
|
226 except AttributeError: |
|
227 self._generate(side) |
|
228 else: |
|
229 method() |
|
230 |
|
231 def _generate(self, side): |
|
232 """generate the backend |
|
233 |
|
234 this is a generic method which will use formats found in self.CORE_SIGNAL_FORMAT |
|
235 and self.CORE_METHOD_FORMAT (standard format method will be used) |
|
236 @param side(str): core or frontend |
|
237 """ |
|
238 side_vars = [] |
|
239 for var in ('FORMATS', 'TEMPLATE', 'DEST'): |
|
240 attr = "{}_{}".format(side.upper(), var) |
|
241 value = getattr(self, attr) |
|
242 if value is None: |
|
243 raise NotImplementedError |
|
244 side_vars.append(value) |
|
245 |
|
246 FORMATS, TEMPLATE, DEST = side_vars |
|
247 del side_vars |
|
248 |
|
249 parts = {part.upper():[] for part in FORMATS} |
|
250 sections = self.bridge_template.sections() |
|
251 sections.sort() |
|
252 for section in sections: |
|
253 function = self.getValues(section) |
|
254 print ("Adding %s %s" % (section, function["type"])) |
|
255 default = self.getDefault(section) |
|
256 arg_doc = self.getArgumentsDoc(section) |
|
257 async_ = "async" in self.getFlags(section) |
|
258 completion = { |
|
259 'sig_in': function['sig_in'] or '', |
|
260 'sig_out': function['sig_out'] or '', |
|
261 'category': 'plugin' if function['category'] == 'plugin' else 'core', |
|
262 'name': section, |
|
263 'args': self.getArguments(function['sig_in'], name=arg_doc, default=default)} |
|
264 |
|
265 extend_method = getattr(self, "{}_completion_{}".format(side, function["type"])) |
|
266 extend_method(completion, function, default, arg_doc, async_) |
|
267 |
|
268 for part, fmt in FORMATS.iteritems(): |
|
269 if part.startswith(function["type"]): |
|
270 parts[part.upper()].append(fmt.format(**completion)) |
|
271 |
|
272 |
|
273 #at this point, signals_part, methods_part and direct_calls should be filled, |
|
274 #we just have to place them in the right part of the template |
|
275 bridge = [] |
|
276 const_override = {env[len(C.ENV_OVERRIDE):]:v for env,v in os.environ.iteritems() if env.startswith(C.ENV_OVERRIDE)} |
|
277 template_path = self.getTemplatePath(TEMPLATE) |
|
278 try: |
|
279 with open(template_path) as template: |
|
280 for line in template: |
|
281 |
|
282 for part, extend_list in parts.iteritems(): |
|
283 if line.startswith('##{}_PART##'.format(part)): |
|
284 bridge.extend(extend_list) |
|
285 break |
|
286 else: |
|
287 # the line is not a magic part replacement |
|
288 if line.startswith('const_'): |
|
289 const_name = line[len('const_'):line.find(' = ')].strip() |
|
290 if const_name in const_override: |
|
291 print("const {} overriden".format(const_name)) |
|
292 bridge.append('const_{} = {}'.format(const_name, const_override[const_name])) |
|
293 continue |
|
294 bridge.append(line.replace('\n', '')) |
|
295 except IOError: |
|
296 print ("can't open template file [{}]".format(template_path)) |
|
297 sys.exit(1) |
|
298 |
|
299 #now we write to final file |
|
300 self.finalWrite(DEST, bridge) |
|
301 |
|
302 def finalWrite(self, filename, file_buf): |
|
303 """Write the final generated file in [dest dir]/filename |
|
304 |
|
305 @param filename: name of the file to generate |
|
306 @param file_buf: list of lines (stings) of the file |
|
307 """ |
|
308 if os.path.exists(self.args.dest_dir) and not os.path.isdir(self.args.dest_dir): |
|
309 print ("The destination dir [%s] can't be created: a file with this name already exists !") |
|
310 sys.exit(1) |
|
311 try: |
|
312 if not os.path.exists(self.args.dest_dir): |
|
313 os.mkdir(self.args.dest_dir) |
|
314 full_path = os.path.join(self.args.dest_dir, filename) |
|
315 if os.path.exists(full_path) and not self.args.force: |
|
316 print ("The destination file [%s] already exists ! Use --force to overwrite it" % full_path) |
|
317 try: |
|
318 with open(full_path, 'w') as dest_file: |
|
319 dest_file.write('\n'.join(file_buf)) |
|
320 except IOError: |
|
321 print ("Can't open destination file [%s]" % full_path) |
|
322 except OSError: |
|
323 print("It's not possible to generate the file, check your permissions") |
|
324 exit(1) |