1""" 2This runtime module contains everything about running 3Python and QtScript scripts inside Scribus. 4 5Look at run_filename for details. 6""" 7import os 8import hashlib 9from ConfigParser import ConfigParser 10 11import sip 12from PyQt4.QtCore import QThread, QObject, QVariant 13from PyQt4.QtGui import qApp, QMessageBox 14from PyQt4.QtScript import QScriptEngine, QScriptValue 15 16from safe_eval import checkCode 17import permitdlg 18 19import __main__ 20 21from inspect import getargspec 22 23class RuntimeConfig(ConfigParser): 24 25 # I cannot use Scripter.preferences because a safe script could 26 # mark other scripts as safe (=allowed) although they use import and 27 # other (possible) dangerous stuff.. 28 # Perhaps I will find a better solution later. 29 30 def __init__(self): 31 ConfigParser.__init__(self) 32 # XXX better use ScPaths->... 33 path = os.path.expanduser("~/.scribus/scripter") 34 if not os.path.exists(path): 35 os.makedirs(path) 36 self.filename = os.path.join(path, "runtime.cfg") 37 self.read([self.filename]) 38 39 40 def save(self): 41 fp = open(self.filename, "w") 42 self.write(fp) 43 fp.close() 44 45 46 def set(self, section, key, value): 47 if not self.has_section(section): 48 self.add_section(section) 49 ConfigParser.set(self, section, key, value) 50 self.save() 51 52 53 def getbool(self, section, key): 54 value = self.get(section, key).strip().lower() 55 if value and value in ["true", "on", "yes", "1"]: 56 return True 57 elif value and value in ["false", "off", "no", "0"]: 58 return False 59 else: 60 raise ValueError, "Invalid boolean value %r" % value 61 62 63runtime_config = RuntimeConfig() 64 65extension_namespace = __main__.__dict__ 66 67 68qts_engine = None 69 70# XXX share namespaces of Python and QtScript 71 72class QtSRuntimeError(Exception): 73 pass 74 75 76def qts_func_decorator(func): 77 def wrapper(context, engine): 78 args = [] 79 (fargs, fvarargs, fvarkw, fdefaults) = getargspec(func) 80 if len(fargs) and fargs[0] == "self": 81 args.append(context.thisObject()) 82 for i in xrange(context.argumentCount()): 83 args.append(context.argument(i)) 84 try: 85 result = func(*args) 86 except Exception, e: 87 # XXX correct behaviour? 88 # http://lists.trolltech.com/qt-interest/2007-06/thread00892-0.html 89 return context.throwValue(QScriptValue(engine, str(e))) 90 if result: 91 return QScriptValue(engine, result) 92 else: 93 return QScriptValue() 94 return wrapper 95 96 97@qts_func_decorator 98def alert(msg_qsv): 99 msg = msg_qsv.toString() 100 QMessageBox.information(Scripter.dialogs.mainWindow.qt, "Alert", msg) 101 102 103def update_qs_namespace(engine, ns): 104 go = engine.globalObject() 105 for name, value in ns.items(): 106 if isinstance(value, QObject): 107 value = engine.newQObject(value) 108 elif callable(value): 109 value = engine.newFunction(value) 110 #elif not isinstance(value, QScriptValue): 111 # value = QScriptValue(engine, value) 112 go.setProperty(name, value) 113 114 115def newQScriptEngine(): 116 engine = QScriptEngine() 117 update_qs_namespace(engine, 118 { 119 "Application": qApp, 120 "Scripter": Scripter.qt, 121 "alert": alert 122 }) 123 return engine 124 125 126def run_qtscript(filename, subroutine=None, extension=False): 127 global qts_engine 128 if not extension: 129 engine = newQScriptEngine() 130 else: 131 engine = qts_engine = qts_engine or newQScriptEngine() 132 code = open(filename).read() 133 engine.clearExceptions() 134 result = engine.evaluate(code) 135 engine.collectGarbage() 136 if not engine.hasUncaughtException() and subroutine: 137 sub = engine.globalObject().property(subroutine) 138 sub.call() 139 if engine.hasUncaughtException(): 140 bt = engine.uncaughtExceptionBacktrace() 141 raise QtSRuntimeError("%s\nTraceback:\%s" % ( 142 str(engine.uncaughtException().toString()), 143 "\n".join([" %s" % l for l in list(bt)]))) 144 145 146def hash_source(filename, source=None): 147 # I gueses sha256 is safe enough without collisions? 148 source = source or open(filename).read() 149 return "%s:%s:%s" % ( 150 os.path.basename(filename), len(filename), hashlib.sha256(source).hexdigest()) 151 152 153def check_python(filename): 154 filename = os.path.abspath(os.path.expanduser(filename)) 155 path = os.path.dirname(filename) 156 # Allow files from global autoload folder by default. 157 # XXX Good idea? 158 if path == os.path.join(Scripter.path, "autoload"): 159 return True 160 code = open(filename).read() 161 h = hash_source(filename, code) 162 if runtime_config.has_option("permissions", h): 163 return runtime_config.getbool("permissions", h) 164 165 problems = checkCode(code) 166 if problems and len(problems) == 1 and isinstance(problems[0], SyntaxError): 167 return True # let's ignore it and let excepthook hande the error later 168 elif problems: 169 ok = permitdlg.ask(filename, problems) 170 if ok == -2: # deny and remember 171 runtime_config.set("permissions", h, False) 172 return False 173 elif ok == 2: # deny 174 return False 175 elif ok == -1: # allow and remember 176 runtime_config.set("permissions", h, True) 177 elif ok == 1: # allow but now remember 178 pass 179 else: 180 raise ValueError, "Inknown return code for permission dialog: %r" % ok 181 return True 182 183 184def run_python(filename, subroutine=None, extension=False): 185 if not extension: 186 namespace = { 187 "__name__": "__scribus__", 188 "__file__": filename 189 } 190 else: 191 namespace = extension_namespace 192 if not check_python(filename): 193 return 194 execfile(filename, namespace) 195 if subroutine: 196 sub = namespace[subroutine] 197 sub() 198 if not extension: 199 del namespace 200 201 202threads = [] 203 204class RunThread(QThread): 205 206 207 def __init__(self, func, *args): 208 QThread.__init__(self, Scripter.qt) 209 self.func = func 210 self.args = args 211 212 213 def run(self): 214 threads.append(self) 215 self.func(*self.args) 216 threads.remove(self) 217 218 219def run_background(func, *args): 220 thread = RunThread(func, *args) 221 thread.start() 222 # XXX: connect done signal with cleanup? 223 return thread 224 225 226 227def mark_keep(): 228 """ 229 mark every child of Scripter.collector to keep 230 """ 231 for child in Scripter.collector.children(): 232 if hasattr(child, "qt"): child = child.qt 233 child.setProperty("keep", QVariant(True)) 234 235 236 237def cleanup(): 238 """ 239 delete every child which is not marked as keep 240 """ 241 for child in Scripter.collector.children(): 242 if hasattr(child, "qt"): child = child.qt 243 v = child.property("keep") 244 if v and v.toBool() == True: 245 #print "Keeping", child 246 continue 247 print "* deleting collected", child 248 sip.delete(child) 249 250 251 252def run_filename(filename, subroutine=None, extension=False, background=False): 253 """ 254 Call this function to run a script and nothing else. 255 It will do everything for you, including garbage collection 256 for QtScript (very simple implementation, see mark_keep and cleanup). 257 Running as extension uses the __main__ namespace and does not 258 delete objects after execution. 259 Running in background as a thread is not much tested and 260 should only be used for non-GUI scripts. 261 """ 262 mark_keep() 263 if background: 264 run_func = run_background 265 else: 266 run_func = lambda func, *args: func(*args) 267 if filename.endswith((".sqts", ".qts", ".sjs", ".js")): 268 run_func(run_qtscript, filename, subroutine, extension) 269 else: 270 run_func(run_python, filename, subroutine, extension) 271 if not background and not extension: 272 # XXX: make sure this is called if an exception occures... 273 cleanup() 274