source: framspy/frams.py @ 1197

Last change on this file since 1197 was 1197, checked in by Maciej Komosinski, 13 months ago

Added the "-t" option in init() to make concurrent calls from different threads sequential, thus making them safe

File size: 15.0 KB
Line 
1"""Framsticks as a Python module.
2
3Static FramScript objects are available inside the module under their well known names
4(frams.Simulator, frams.GenePools, etc.)
5
6These objects and all values passed to and from Framsticks are instances of frams.ExtValue.
7Python values are automatically converted to Framstics data types.
8Use frams.ExtValue._makeInt()/_makeDouble()/_makeString()/_makeNull() for explicit conversions.
9Simple values returned from Framsticks can be converted to their natural Python
10counterparts using _value() (or forced to a specific type with  _int()/_double()/_string()).
11
12All non-Framsticks Python attributes start with '_' to avoid conflicts with Framsticks attributes.
13Framsticks names that are Python reserved words are prefixed with 'x' (currently just Simulator.ximport).
14
15For sample usage, see frams-test.py and FramsticksLib.py.
16
17If you want to run many independent instances of this class in parallel, use the "multiprocessing" module and then each process
18that uses this module will initialize it and get access to a separate instance of the Framsticks library.
19
20If you want to use this module from multiple threads concurrently, use the "-t" option for init().
21This will make concurrent calls from different threads sequential, thus making them safe.
22However, this will likely degrade the performance (due to required locking) compared to the single-threaded use.
23
24For interfaces in other languages (e.g. using the Framsticks library in your C++ code), see ../cpp/frams/frams-objects.h
25"""
26
27import ctypes, re, sys, os
28
29c_api = None  # will be initialized in init(). Global because ExtValue uses it.
30
31
32class ExtValue(object):
33        """All Framsticks objects and values are instances of this class. Read the documentation of the 'frams' module for more information."""
34
35        _reInsideParens = re.compile('\((.*)\)')
36        _reservedWords = ['import']  # this list is scanned during every attribute access, only add what is really clashing with Framsticks properties
37        _reservedXWords = ['x' + word for word in _reservedWords]
38        _encoding = 'utf-8'
39
40
41        def __init__(self, arg=None, dontinit=False):
42                if dontinit:
43                        return
44                if isinstance(arg, int):
45                        self._initFromInt(arg)
46                elif isinstance(arg, str):
47                        self._initFromString(arg)
48                elif isinstance(arg, float):
49                        self._initFromDouble(arg)
50                elif arg == None:
51                        self._initFromNull()
52                else:
53                        raise ctypes.ArgumentError("Can't make ExtValue from '%s' (%s)" % (str(arg), type(arg)))
54
55
56        def __del__(self):
57                c_api.extFree(self.__ptr)
58
59
60        def _initFromNull(self):
61                self.__ptr = c_api.extFromNull()
62
63
64        def _initFromInt(self, v):
65                self.__ptr = c_api.extFromInt(v)
66
67
68        def _initFromDouble(self, v):
69                self.__ptr = c_api.extFromDouble(v)
70
71
72        def _initFromString(self, v):
73                self.__ptr = c_api.extFromString(ExtValue._cstringFromPython(v))
74
75
76        @classmethod
77        def _makeNull(cls, v):
78                e = ExtValue(None, True)
79                e._initFromNull()
80                return e
81
82
83        @classmethod
84        def _makeInt(cls, v):
85                e = ExtValue(None, True)
86                e._initFromInt(v)
87                return e
88
89
90        @classmethod
91        def _makeDouble(cls, v):
92                e = ExtValue(None, True)
93                e._initFromDouble(v)
94                return e
95
96
97        @classmethod
98        def _makeString(cls, v):
99                e = ExtValue(None, True)
100                e._initFromString(v)
101                return e
102
103
104        @classmethod
105        def _rootObject(cls):
106                e = ExtValue(None, True)
107                e.__ptr = c_api.rootObject()
108                return e
109
110
111        @classmethod
112        def _stringFromC(cls, cptr):
113                return cptr.decode(ExtValue._encoding)
114
115
116        @classmethod
117        def _cstringFromPython(cls, s):
118                return ctypes.c_char_p(s.encode(ExtValue._encoding))
119
120
121        def _type(self):
122                return c_api.extType(self.__ptr)
123
124
125        def _class(self):
126                cls = c_api.extClass(self.__ptr)
127                if cls == None:
128                        return None
129                else:
130                        return ExtValue._stringFromC(cls)
131
132
133        def _value(self):
134                t = self._type()
135                if t == 0:
136                        return None
137                elif t == 1:
138                        return self._int()
139                elif t == 2:
140                        return self._double()
141                elif t == 3:
142                        return self._string()
143                else:
144                        return self
145
146
147        def _int(self):
148                return c_api.extIntValue(self.__ptr)
149
150
151        def _double(self):
152                return c_api.extDoubleValue(self.__ptr)
153
154
155        def _string(self):
156                return ExtValue._stringFromC(c_api.extStringValue(self.__ptr))
157
158
159        def _propCount(self):
160                return c_api.extPropCount(self.__ptr)
161
162
163        def _propId(self, i):
164                return ExtValue._stringFromC(c_api.extPropId(self.__ptr, i))
165
166
167        def _propName(self, i):
168                return ExtValue._stringFromC(c_api.extPropName(self.__ptr, i))
169
170
171        def _propType(self, i):
172                return ExtValue._stringFromC(c_api.extPropType(self.__ptr, i))
173
174
175        def _propHelp(self, i):
176                h = c_api.extPropHelp(self.__ptr, i)  # unlike other string fields, help is sometimes NULL
177                return ExtValue._stringFromC(h) if h != None else '';
178
179
180        def _propFlags(self, i):
181                return c_api.extPropFlags(self.__ptr, i)
182
183
184        def _propGroup(self, i):
185                return c_api.extPropGroup(self.__ptr, i)
186
187
188        def _groupCount(self):
189                return c_api.extGroupCount(self.__ptr)
190
191
192        def _groupName(self, i):
193                return ExtValue._stringFromC(c_api.extGroupName(self.__ptr, i))
194
195
196        def _groupMember(self, g, i):
197                return c_api.extGroupMember(self.__ptr, g, i)
198
199
200        def _memberCount(self, g):
201                return c_api.extMemberCount(self.__ptr, g)
202
203
204        def __str__(self):
205                return self._string()
206
207
208        def __dir__(self):
209                ids = dir(type(self))
210                if self._type() == 4:
211                        for i in range(c_api.extPropCount(self.__ptr)):
212                                name = ExtValue._stringFromC(c_api.extPropId(self.__ptr, i))
213                                if name in ExtValue._reservedWords:
214                                        name = 'x' + name
215                                ids.append(name)
216                return ids
217
218
219        def __getattr__(self, key):
220                if key[0] == '_':
221                        return self.__dict__[key]
222                if key in ExtValue._reservedXWords:
223                        key = key[1:]
224                prop_i = c_api.extPropFind(self.__ptr, ExtValue._cstringFromPython(key))
225                if prop_i < 0:
226                        raise AttributeError('no ' + str(key) + ' in ' + str(self))
227                t = ExtValue._stringFromC(c_api.extPropType(self.__ptr, prop_i))
228                if t[0] == 'p':
229                        arg_types = ExtValue._reInsideParens.search(t)
230                        if arg_types:
231                                arg_types = arg_types.group(1).split(',')  # anyone wants to add argument type validation using param type declarations?
232
233
234                        def fun(*args):
235                                ext_args = []
236                                ext_pointers = []
237                                for a in args:
238                                        if isinstance(a, ExtValue):
239                                                ext = a
240                                        else:
241                                                ext = ExtValue(a)
242                                        ext_args.append(ext)
243                                        ext_pointers.append(ext.__ptr)
244                                ret = ExtValue(None, True)
245                                args_array = (ctypes.c_void_p * len(args))(*ext_pointers)
246                                ret.__ptr = c_api.extPropCall(self.__ptr, prop_i, len(args), args_array)
247                                return ret
248
249
250                        return fun
251                else:
252                        ret = ExtValue(None, True)
253                        ret.__ptr = c_api.extPropGet(self.__ptr, prop_i)
254                        return ret
255
256
257        def __setattr__(self, key, value):
258                if key[0] == '_':
259                        self.__dict__[key] = value
260                else:
261                        if key in ExtValue._reservedXWords:
262                                key = key[1:]
263                        prop_i = c_api.extPropFind(self.__ptr, ExtValue._cstringFromPython(key))
264                        if prop_i < 0:
265                                raise AttributeError("No '" + str(key) + "' in '" + str(self) + "'")
266                        if not isinstance(value, ExtValue):
267                                value = ExtValue(value)
268                        c_api.extPropSet(self.__ptr, prop_i, value.__ptr)
269
270
271        def __getitem__(self, key):
272                return self.get(key)
273
274
275        def __setitem__(self, key, value):
276                return self.set(key, value)
277
278
279        def __len__(self):
280                try:
281                        return self.size._int()
282                except:
283                        return 0
284
285
286        def __iter__(self):
287                class It(object):
288                        def __init__(self, container, frams_it):
289                                self.container = container
290                                self.frams_it = frams_it
291
292
293                        def __iter__(self):
294                                return self
295
296
297                        def __next__(self):
298                                if self.frams_it.next._int() != 0:
299                                        return self.frams_it.value
300                                else:
301                                        raise StopIteration()
302
303                return It(self, self.iterator)
304
305
306def init(*args):
307        """
308        Initializes the connection to Framsticks dll/so/dylib.
309
310        Python programs do not have to know the Framstics path but if they know, just pass the path as the first argument.
311        Similarly '-dPATH' and '-DPATH' needed by Framsticks are optional and derived from the first path, unless they are specified as args in init().
312        '-LNAME' is the optional library name (full name including the file name extension), default is 'frams-objects.dll/.so' depending on the platform.
313        All other arguments are passed to Framsticks and not interpreted by this function.
314        """
315
316        frams_d = None
317        frams_D = None
318        lib_path = None
319        lib_name = ('frams-objects.dylib' if sys.platform == 'darwin' else 'frams-objects.so') if os.name == 'posix' else 'frams-objects.dll'
320        initargs = []
321        for a in args:
322                if a[:2] == '-d':
323                        frams_d = a
324                elif a[:2] == '-D':
325                        frams_D = a
326                elif a[:2] == '-L':
327                        lib_name = a[2:]
328                elif a[:2] == '-t':
329                        print("frams.py: thread synchronization enabled.")  # Due to performance penalty, only use if you are really calling methods from different threads.
330                        from functools import wraps
331                        from threading import RLock
332                       
333                        def threads_synchronized(lock):
334                                def wrapper(f):
335                                        @wraps(f)
336                                        def inner_wrapper(*args, **kwargs):
337                                                with lock:
338                                                        return f(*args, **kwargs)
339                                        return inner_wrapper
340                                return wrapper
341
342                        thread_synchronizer = threads_synchronized(RLock())
343                        for name in ExtValue.__dict__:
344                                attr = getattr(ExtValue, name)
345                                if callable(attr) and attr:  # decorate all methods of ExtValue with a reentrant lock so that different threads do not use them concurrently
346                                        setattr(ExtValue, name, thread_synchronizer(attr))
347                elif lib_path is None:
348                        lib_path = a
349                else:
350                        initargs.append(a)
351        if lib_path is None:
352                # TODO: use environment variable and/or the zip distribution we are in when the path is not specified in arg
353                # for now just assume the current dir is Framsticks
354                lib_path = '.'
355
356        if os.name == 'nt':
357                if sys.version_info < (3, 8):
358                        original_dir = os.getcwd()
359                        os.chdir(lib_path)  # because under Windows, frams-objects.dll requires other dll's which reside in the same directory, so we must change current dir for them to be found while loading the main dll.
360                else:
361                        os.add_dll_directory(os.path.abspath(lib_path))
362        abs_data = os.path.join(os.path.abspath(lib_path), "data")  # use absolute path for -d and -D so python is free to cd anywhere without confusing Framsticks
363
364        # for the hypothetical case without lib_path, the abs_data must be obtained from somewhere else
365        if frams_d is None:
366                frams_d = '-d' + abs_data
367        if frams_D is None:
368                frams_D = '-D' + abs_data
369        initargs.insert(0, frams_d)
370        initargs.insert(0, frams_D)
371        initargs.insert(0, 'dummy.exe')  # as an offset, 0th arg is by convention app name
372
373        global c_api  # access global variable
374        if lib_path is not None:  # theoretically, this should only be needed for "and os.name == 'posix'", but in windows python 3.9.5, without using the full lib_name path, there is FileNotFoundError: Could not find module 'frams-objects.dll' (or one of its dependencies). Try using the full path with constructor syntax. Maybe related: https://bugs.python.org/issue42114 and https://stackoverflow.com/questions/59330863/cant-import-dll-module-in-python and https://bugs.python.org/issue39393
375                lib_name = os.path.join(lib_path, lib_name)  # lib_path is always set ('.' when not specified). For the (currently unused) case of lib_path==None, the resulting lib_name is a bare filename without any path, which loads the library from a system-configured location.
376        try:
377                c_api = ctypes.CDLL(lib_name)  # if accessing this module from multiple threads, they will all share a single c_api and access the same copy of the library and its data. If you want separate independent copies, read the comment at the top of this file on using the "multiprocessing" module.
378        except OSError as e:
379                print("*** Could not find or open '%s' from '%s'.\n*** Did you provide proper arguments and is this file readable?\n" % (lib_name, os.getcwd()))
380                raise
381
382        if os.name == 'nt' and sys.version_info < (3, 8):
383                os.chdir(original_dir)  # restore current working dir after loading the library so Framsticks sees the expected directory
384
385        c_api.init.argtypes = [ctypes.c_int, ctypes.POINTER(ctypes.c_char_p)]
386        c_api.init.restype = None
387        c_api.extFree.argtypes = [ctypes.c_void_p]
388        c_api.extFree.restype = None
389        c_api.extType.argtypes = [ctypes.c_void_p]
390        c_api.extType.restype = ctypes.c_int
391        c_api.extFromNull.argtypes = []
392        c_api.extFromNull.restype = ctypes.c_void_p
393        c_api.extFromInt.argtypes = [ctypes.c_int]
394        c_api.extFromInt.restype = ctypes.c_void_p
395        c_api.extFromDouble.argtypes = [ctypes.c_double]
396        c_api.extFromDouble.restype = ctypes.c_void_p
397        c_api.extFromString.argtypes = [ctypes.c_char_p]
398        c_api.extFromString.restype = ctypes.c_void_p
399        c_api.extIntValue.argtypes = [ctypes.c_void_p]
400        c_api.extIntValue.restype = ctypes.c_int
401        c_api.extDoubleValue.argtypes = [ctypes.c_void_p]
402        c_api.extDoubleValue.restype = ctypes.c_double
403        c_api.extStringValue.argtypes = [ctypes.c_void_p]
404        c_api.extStringValue.restype = ctypes.c_char_p
405        c_api.extClass.argtypes = [ctypes.c_void_p]
406        c_api.extClass.restype = ctypes.c_char_p
407        c_api.extPropCount.argtypes = [ctypes.c_void_p]
408        c_api.extPropCount.restype = ctypes.c_int
409        c_api.extPropId.argtypes = [ctypes.c_void_p, ctypes.c_int]
410        c_api.extPropId.restype = ctypes.c_char_p
411        c_api.extPropName.argtypes = [ctypes.c_void_p, ctypes.c_int]
412        c_api.extPropName.restype = ctypes.c_char_p
413        c_api.extPropType.argtypes = [ctypes.c_void_p, ctypes.c_int]
414        c_api.extPropType.restype = ctypes.c_char_p
415        c_api.extPropGroup.argtypes = [ctypes.c_void_p, ctypes.c_int]
416        c_api.extPropGroup.restype = ctypes.c_int
417        c_api.extPropFlags.argtypes = [ctypes.c_void_p, ctypes.c_int]
418        c_api.extPropFlags.restype = ctypes.c_int
419        c_api.extPropHelp.argtypes = [ctypes.c_void_p, ctypes.c_int]
420        c_api.extPropHelp.restype = ctypes.c_char_p
421        c_api.extPropFind.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
422        c_api.extPropFind.restype = ctypes.c_int
423        c_api.extPropGet.argtypes = [ctypes.c_void_p, ctypes.c_int]
424        c_api.extPropGet.restype = ctypes.c_void_p
425        c_api.extPropSet.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p]
426        c_api.extPropSet.restype = ctypes.c_int
427        c_api.extPropCall.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_int, ctypes.c_void_p]
428        c_api.extPropCall.restype = ctypes.c_void_p
429        c_api.extGroupCount.argtypes = [ctypes.c_void_p]
430        c_api.extGroupCount.restype = ctypes.c_int
431        c_api.extGroupName.argtypes = [ctypes.c_void_p, ctypes.c_int]
432        c_api.extGroupName.restype = ctypes.c_char_p
433        c_api.extGroupMember.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_int]
434        c_api.extGroupMember.restype = ctypes.c_int
435        c_api.extMemberCount.argtypes = [ctypes.c_void_p, ctypes.c_int]
436        c_api.extMemberCount.restype = ctypes.c_int
437        c_api.rootObject.argtypes = []
438        c_api.rootObject.restype = ctypes.c_void_p
439
440        c_args = (ctypes.c_char_p * len(initargs))(*list(a.encode(ExtValue._encoding) for a in initargs))
441        c_api.init(len(initargs), c_args)
442
443        Root = ExtValue._rootObject()
444        for n in dir(Root):
445                if n[0].isalpha():
446                        attr = getattr(Root, n)
447                        if isinstance(attr, ExtValue):
448                                attr = attr._value()
449                        setattr(sys.modules[__name__], n, attr)
450
451        print('Using Framsticks version: ' + str(Simulator.version_string))
452        print('Home (writable) dir     : ' + home_dir)
453        print('Resources dir           : ' + res_dir)
454        print()
Note: See TracBrowser for help on using the repository browser.