comparison sat/bridge/bridge_constructor/base_constructor.py @ 2562:26edcf3a30eb

core, setup: huge cleaning: - moved directories from src and frontends/src to sat and sat_frontends, which is the recommanded naming convention - move twisted directory to root - removed all hacks from setup.py, and added missing dependencies, it is now clean - use https URL for website in setup.py - removed "Environment :: X11 Applications :: GTK", as wix is deprecated and removed - renamed sat.sh to sat and fixed its installation - added python_requires to specify Python version needed - replaced glib2reactor which use deprecated code by gtk3reactor sat can now be installed directly from virtualenv without using --system-site-packages anymore \o/
author Goffi <goffi@goffi.org>
date Mon, 02 Apr 2018 19:44:50 +0200
parents src/bridge/bridge_constructor/base_constructor.py@0046283a285d
children 56f94936df1e
comparison
equal deleted inserted replaced
2561:bd30dc3ffe5a 2562:26edcf3a30eb
1 #!/usr/bin/env python2
2 #-*- coding: utf-8 -*-
3
4 # SàT: a XMPP client
5 # Copyright (C) 2009-2018 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 # set to False if your bridge need only core
53 FRONTEND_ACTIVATE = True
54
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)
176 @param name: dictionary of arguments name like given by getArgumentsDoc
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))
179 @return (str): arguments that correspond to a signature (e.g.: "sss" return "arg1, arg2, arg3")
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":
228 if not self.FRONTEND_ACTIVATE:
229 print(u"This constructor only handle core, please use core side")
230 sys.exit(1)
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,
269 # arguments with default values
270 'args': self.getArguments(function['sig_in'], name=arg_doc, default=default),
271 }
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)