Mercurial > libervia-backend
annotate src/bridge/bridge_constructor/base_constructor.py @ 2156:8f96c242fa89
jp (outputs): added OUTPUT_COMPLEX type for data which are more complex than key/value dictionaries
author | Goffi <goffi@goffi.org> |
---|---|
date | Thu, 16 Feb 2017 00:51:33 +0100 |
parents | f413bfc24458 |
children | 8b37a62336c3 |
rev | line source |
---|---|
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 | |
2087
159250d66407
bridge (constructor): embedded bridge generator:
Goffi <goffi@goffi.org>
parents:
2085
diff
changeset
|
52 # set to False if your bridge need only core |
159250d66407
bridge (constructor): embedded bridge generator:
Goffi <goffi@goffi.org>
parents:
2085
diff
changeset
|
53 FRONTEND_ACTIVATE = True |
159250d66407
bridge (constructor): embedded bridge generator:
Goffi <goffi@goffi.org>
parents:
2085
diff
changeset
|
54 |
2085 | 55 def __init__(self, bridge_template, options): |
56 self.bridge_template = bridge_template | |
57 self.args = options | |
58 | |
59 @property | |
60 def constructor_dir(self): | |
61 constructor_mod = import_module(self.__module__) | |
62 return os.path.dirname(constructor_mod.__file__) | |
63 | |
64 def getValues(self, name): | |
65 """Return values of a function in a dict | |
66 @param name: Name of the function to get | |
67 @return: dict, each key has the config value or None if the value is not set""" | |
68 function = {} | |
69 for option in ['type', 'category', 'sig_in', 'sig_out', 'doc']: | |
70 try: | |
71 value = self.bridge_template.get(name, option) | |
72 except NoOptionError: | |
73 value = None | |
74 function[option] = value | |
75 return function | |
76 | |
77 def getDefault(self, name): | |
78 """Return default values of a function in a dict | |
79 @param name: Name of the function to get | |
80 @return: dict, each key is the integer param number (no key if no default value)""" | |
81 default_dict = {} | |
82 def_re = re.compile(r"param_(\d+)_default") | |
83 | |
84 for option in self.bridge_template.options(name): | |
85 match = def_re.match(option) | |
86 if match: | |
87 try: | |
88 idx = int(match.group(1)) | |
89 except ValueError: | |
90 raise ParseError("Invalid value [%s] for parameter number" % match.group(1)) | |
91 default_dict[idx] = self.bridge_template.get(name, option) | |
92 | |
93 return default_dict | |
94 | |
95 def getFlags(self, name): | |
96 """Return list of flags set for this function | |
97 | |
98 @param name: Name of the function to get | |
99 @return: List of flags (string) | |
100 """ | |
101 flags = [] | |
102 for option in self.bridge_template.options(name): | |
103 if option in C.DECLARATION_FLAGS: | |
104 flags.append(option) | |
105 return flags | |
106 | |
107 def getArgumentsDoc(self, name): | |
108 """Return documentation of arguments | |
109 @param name: Name of the function to get | |
110 @return: dict, each key is the integer param number (no key if no argument doc), value is a tuple (name, doc)""" | |
111 doc_dict = {} | |
112 option_re = re.compile(r"doc_param_(\d+)") | |
113 value_re = re.compile(r"^(\w+): (.*)$", re.MULTILINE | re.DOTALL) | |
114 for option in self.bridge_template.options(name): | |
115 if option == 'doc_return': | |
116 doc_dict['return'] = self.bridge_template.get(name, option) | |
117 continue | |
118 match = option_re.match(option) | |
119 if match: | |
120 try: | |
121 idx = int(match.group(1)) | |
122 except ValueError: | |
123 raise ParseError("Invalid value [%s] for parameter number" % match.group(1)) | |
124 value_match = value_re.match(self.bridge_template.get(name, option)) | |
125 if not value_match: | |
126 raise ParseError("Invalid value for parameter doc [%i]" % idx) | |
127 doc_dict[idx] = (value_match.group(1), value_match.group(2)) | |
128 return doc_dict | |
129 | |
130 def getDoc(self, name): | |
131 """Return documentation of the method | |
132 @param name: Name of the function to get | |
133 @return: string documentation, or None""" | |
134 if self.bridge_template.has_option(name, "doc"): | |
135 return self.bridge_template.get(name, "doc") | |
136 return None | |
137 | |
138 def argumentsParser(self, signature): | |
139 """Generator which return individual arguments signatures from a global signature""" | |
140 start = 0 | |
141 i = 0 | |
142 | |
143 while i < len(signature): | |
144 if signature[i] not in ['b', 'y', 'n', 'i', 'x', 'q', 'u', 't', 'd', 's', 'a']: | |
145 raise ParseError("Unmanaged attribute type [%c]" % signature[i]) | |
146 | |
147 if signature[i] == 'a': | |
148 i += 1 | |
149 if signature[i] != '{' and signature[i] != '(': # FIXME: must manage tuples out of arrays | |
150 i += 1 | |
151 yield signature[start:i] | |
152 start = i | |
153 continue # we have a simple type for the array | |
154 opening_car = signature[i] | |
155 assert(opening_car in ['{', '(']) | |
156 closing_car = '}' if opening_car == '{' else ')' | |
157 opening_count = 1 | |
158 while (True): # we have a dict or a list of tuples | |
159 i += 1 | |
160 if i >= len(signature): | |
161 raise ParseError("missing }") | |
162 if signature[i] == opening_car: | |
163 opening_count += 1 | |
164 if signature[i] == closing_car: | |
165 opening_count -= 1 | |
166 if opening_count == 0: | |
167 break | |
168 i += 1 | |
169 yield signature[start:i] | |
170 start = i | |
171 | |
172 def getArguments(self, signature, name=None, default=None, unicode_protect=False): | |
173 """Return arguments to user given a signature | |
174 | |
175 @param signature: signature in the short form (using s,a,i,b etc) | |
2087
159250d66407
bridge (constructor): embedded bridge generator:
Goffi <goffi@goffi.org>
parents:
2085
diff
changeset
|
176 @param name: dictionary of arguments name like given by getArgumentsDoc |
2085 | 177 @param default: dictionary of default values, like given by getDefault |
178 @param unicode_protect: activate unicode protection on strings (return strings as unicode(str)) | |
2091
f413bfc24458
bridge, quick_frontend: preparation for async bridge
Goffi <goffi@goffi.org>
parents:
2087
diff
changeset
|
179 @return (str): arguments that correspond to a signature (e.g.: "sss" return "arg1, arg2, arg3") |
2085 | 180 """ |
181 idx = 0 | |
182 attr_string = [] | |
183 | |
184 for arg in self.argumentsParser(signature): | |
185 attr_string.append(("unicode(%(name)s)%(default)s" if (unicode_protect and arg == 's') else "%(name)s%(default)s") % { | |
186 'name': name[idx][0] if (name and idx in name) else "arg_%i" % idx, | |
187 'default': "=" + default[idx] if (default and idx in default) else ''}) | |
188 # give arg_1, arg2, etc or name1, name2=default, etc. | |
189 #give unicode(arg_1), unicode(arg_2), etc. if unicode_protect is set and arg is a string | |
190 idx += 1 | |
191 | |
192 return ", ".join(attr_string) | |
193 | |
194 def getTemplatePath(self, template_file): | |
195 """return template path corresponding to file name | |
196 | |
197 @param template_file(str): name of template file | |
198 """ | |
199 return os.path.join(self.constructor_dir, template_file) | |
200 | |
201 def core_completion_method(self, completion, function, default, arg_doc, async_): | |
202 """override this method to extend completion""" | |
203 pass | |
204 | |
205 def core_completion_signal(self, completion, function, default, arg_doc, async_): | |
206 """override this method to extend completion""" | |
207 pass | |
208 | |
209 def frontend_completion_method(self, completion, function, default, arg_doc, async_): | |
210 """override this method to extend completion""" | |
211 pass | |
212 | |
213 def frontend_completion_signal(self, completion, function, default, arg_doc, async_): | |
214 """override this method to extend completion""" | |
215 pass | |
216 | |
217 | |
218 def generate(self, side): | |
219 """generate bridge | |
220 | |
221 call generateCoreSide or generateFrontendSide if they exists | |
222 else call generic self._generate method | |
223 """ | |
224 try: | |
225 if side == "core": | |
226 method = self.generateCoreSide | |
227 elif side == "frontend": | |
2087
159250d66407
bridge (constructor): embedded bridge generator:
Goffi <goffi@goffi.org>
parents:
2085
diff
changeset
|
228 if not self.FRONTEND_ACTIVATE: |
159250d66407
bridge (constructor): embedded bridge generator:
Goffi <goffi@goffi.org>
parents:
2085
diff
changeset
|
229 print(u"This constructor only handle core, please use core side") |
159250d66407
bridge (constructor): embedded bridge generator:
Goffi <goffi@goffi.org>
parents:
2085
diff
changeset
|
230 sys.exit(1) |
2085 | 231 method = self.generateFrontendSide |
232 except AttributeError: | |
233 self._generate(side) | |
234 else: | |
235 method() | |
236 | |
237 def _generate(self, side): | |
238 """generate the backend | |
239 | |
240 this is a generic method which will use formats found in self.CORE_SIGNAL_FORMAT | |
241 and self.CORE_METHOD_FORMAT (standard format method will be used) | |
242 @param side(str): core or frontend | |
243 """ | |
244 side_vars = [] | |
245 for var in ('FORMATS', 'TEMPLATE', 'DEST'): | |
246 attr = "{}_{}".format(side.upper(), var) | |
247 value = getattr(self, attr) | |
248 if value is None: | |
249 raise NotImplementedError | |
250 side_vars.append(value) | |
251 | |
252 FORMATS, TEMPLATE, DEST = side_vars | |
253 del side_vars | |
254 | |
255 parts = {part.upper():[] for part in FORMATS} | |
256 sections = self.bridge_template.sections() | |
257 sections.sort() | |
258 for section in sections: | |
259 function = self.getValues(section) | |
260 print ("Adding %s %s" % (section, function["type"])) | |
261 default = self.getDefault(section) | |
262 arg_doc = self.getArgumentsDoc(section) | |
263 async_ = "async" in self.getFlags(section) | |
264 completion = { | |
265 'sig_in': function['sig_in'] or '', | |
266 'sig_out': function['sig_out'] or '', | |
267 'category': 'plugin' if function['category'] == 'plugin' else 'core', | |
268 'name': section, | |
2087
159250d66407
bridge (constructor): embedded bridge generator:
Goffi <goffi@goffi.org>
parents:
2085
diff
changeset
|
269 # arguments with default values |
159250d66407
bridge (constructor): embedded bridge generator:
Goffi <goffi@goffi.org>
parents:
2085
diff
changeset
|
270 'args': self.getArguments(function['sig_in'], name=arg_doc, default=default), |
159250d66407
bridge (constructor): embedded bridge generator:
Goffi <goffi@goffi.org>
parents:
2085
diff
changeset
|
271 } |
2085 | 272 |
273 extend_method = getattr(self, "{}_completion_{}".format(side, function["type"])) | |
274 extend_method(completion, function, default, arg_doc, async_) | |
275 | |
276 for part, fmt in FORMATS.iteritems(): | |
277 if part.startswith(function["type"]): | |
278 parts[part.upper()].append(fmt.format(**completion)) | |
279 | |
280 | |
281 #at this point, signals_part, methods_part and direct_calls should be filled, | |
282 #we just have to place them in the right part of the template | |
283 bridge = [] | |
284 const_override = {env[len(C.ENV_OVERRIDE):]:v for env,v in os.environ.iteritems() if env.startswith(C.ENV_OVERRIDE)} | |
285 template_path = self.getTemplatePath(TEMPLATE) | |
286 try: | |
287 with open(template_path) as template: | |
288 for line in template: | |
289 | |
290 for part, extend_list in parts.iteritems(): | |
291 if line.startswith('##{}_PART##'.format(part)): | |
292 bridge.extend(extend_list) | |
293 break | |
294 else: | |
295 # the line is not a magic part replacement | |
296 if line.startswith('const_'): | |
297 const_name = line[len('const_'):line.find(' = ')].strip() | |
298 if const_name in const_override: | |
299 print("const {} overriden".format(const_name)) | |
300 bridge.append('const_{} = {}'.format(const_name, const_override[const_name])) | |
301 continue | |
302 bridge.append(line.replace('\n', '')) | |
303 except IOError: | |
304 print ("can't open template file [{}]".format(template_path)) | |
305 sys.exit(1) | |
306 | |
307 #now we write to final file | |
308 self.finalWrite(DEST, bridge) | |
309 | |
310 def finalWrite(self, filename, file_buf): | |
311 """Write the final generated file in [dest dir]/filename | |
312 | |
313 @param filename: name of the file to generate | |
314 @param file_buf: list of lines (stings) of the file | |
315 """ | |
316 if os.path.exists(self.args.dest_dir) and not os.path.isdir(self.args.dest_dir): | |
317 print ("The destination dir [%s] can't be created: a file with this name already exists !") | |
318 sys.exit(1) | |
319 try: | |
320 if not os.path.exists(self.args.dest_dir): | |
321 os.mkdir(self.args.dest_dir) | |
322 full_path = os.path.join(self.args.dest_dir, filename) | |
323 if os.path.exists(full_path) and not self.args.force: | |
324 print ("The destination file [%s] already exists ! Use --force to overwrite it" % full_path) | |
325 try: | |
326 with open(full_path, 'w') as dest_file: | |
327 dest_file.write('\n'.join(file_buf)) | |
328 except IOError: | |
329 print ("Can't open destination file [%s]" % full_path) | |
330 except OSError: | |
331 print("It's not possible to generate the file, check your permissions") | |
332 exit(1) |