1# commandinterpreter.py 2# this module handles the command line interface interpreter 3 4# Copyright (C) 2004 Jeremy S. Sanders 5# Email: Jeremy Sanders <jeremy@jeremysanders.net> 6# 7# This program is free software; you can redistribute it and/or modify 8# it under the terms of the GNU General Public License as published by 9# the Free Software Foundation; either version 2 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 General Public License for more details. 16# 17# You should have received a copy of the GNU General Public License along 18# with this program; if not, write to the Free Software Foundation, Inc., 19# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 20############################################################################## 21 22""" 23A module for the execution of user 'macro' code inside a special 24environment. That way the commandline can be used to interact with 25the app without worrying about the app internals. 26 27The way this works is to create an evironment specific to the class 28consisting of globals & locals. 29 30Commands can be run inside the environment. Python errors are trapped 31and dumped out to stderr. 32 33stderr and stdout can be reassigned in the environment to point to 34an alternative interface. They should point to objects providing 35a write() interface. 36 37This class is modelled on the one described in 38'GUI Programming in Python: QT Edition' (Boudewijn Rempt) 39""" 40 41from __future__ import division, print_function 42 43# get globals before things are imported 44_globals = globals() 45 46import sys 47import traceback 48import io 49import os.path 50 51from ..compat import pickle, cexec, cpy3 52from .commandinterface import CommandInterface 53from .. import utils 54 55class CommandInterpreter(object): 56 """Class for executing commands in the Veusz command line language.""" 57 58 def __init__(self, document): 59 """ Initialise object with the document it interfaces.""" 60 self.document = document 61 62 # set up interface to document 63 self.interface = CommandInterface(document) 64 65 # initialise environment (make a copy from inital globals) 66 self.globals = _globals.copy() 67 68 # save the stdout & stderr 69 self.write_stdout = sys.stdout 70 self.write_stderr = sys.stderr 71 self.read_stdin = sys.stdin 72 73 # import numpy into the environment 74 cexec("from numpy import *", self.globals) 75 76 # define root object 77 self.globals['Root'] = self.interface.Root 78 79 # shortcut 80 ifc = self.interface 81 82 # define commands for interface 83 self.cmds = {} 84 for cmd in ( 85 CommandInterface.safe_commands + 86 CommandInterface.unsafe_commands + 87 CommandInterface.import_commands): 88 self.cmds[cmd] = getattr(ifc, cmd) 89 self.cmds['GPL'] = self.GPL 90 self.cmds['Load'] = self.Load 91 92 self.globals.update( self.cmds ) 93 94 def addCommand(self, name, command): 95 """Add the given command to the list of available commands.""" 96 self.cmds[name] = command 97 self.globals[name] = command 98 99 def setFiles(self, stdout, stderr, stdin): 100 """Assign the environment input/output files.""" 101 self.write_stdout = stdout 102 self.write_stderr = stderr 103 self.read_stdin = stdin 104 105 def _pythonise(self, text): 106 """Internal routine to convert commands in the form Cmd a b c into Cmd(a,b,c).""" 107 108 out = '' 109 # iterate over lines 110 for line in text.split('\n'): 111 parts = line.split() 112 113 # turn Cmd a b c into Cmd(a,b,c) 114 if len(parts) != 0 and parts[0] in self.cmds: 115 line = utils.pythonise(line) 116 117 out += line + '\n' 118 119 return out 120 121 def run(self, inputcmds, filename = None): 122 """ Run a set of commands inside the preserved environment. 123 124 inputcmds: a string with the commands to run 125 filename: a filename to report if there are errors 126 """ 127 128 if filename is None: 129 filename = '<string>' 130 131 # pythonise! 132 inputcmds = self._pythonise(inputcmds) 133 134 # ignore if blank 135 if len(inputcmds.strip()) == 0: 136 return 137 138 # preserve output streams 139 saved = sys.stdout, sys.stderr, sys.stdin 140 sys.stdout, sys.stderr, sys.stdin = ( 141 self.write_stdout, self.write_stderr, self.read_stdin) 142 143 # count number of newlines in expression 144 # If it's 2, then execute as a single statement (print out result) 145 if inputcmds.count('\n') == 2: 146 stattype = 'single' 147 else: 148 stattype = 'exec' 149 150 # first compile the code to check for syntax errors 151 try: 152 c = compile(inputcmds, filename, stattype) 153 except (OverflowError, ValueError, SyntaxError): 154 info = sys.exc_info() 155 backtrace = traceback.format_exception(*info) 156 for line in backtrace: 157 sys.stderr.write(line) 158 159 else: 160 # block update signals from document while updating 161 with self.document.suspend(): 162 try: 163 # execute the code 164 cexec(c, self.globals) 165 except: 166 # print out the backtrace to stderr 167 info = sys.exc_info() 168 backtrace = traceback.format_exception(*info) 169 for line in backtrace: 170 sys.stderr.write(line) 171 172 # return output streams 173 sys.stdout, sys.stderr, sys.stdin = saved 174 175 def Load(self, filename): 176 """Replace the document with a new one from the filename.""" 177 178 mode = 'r' if cpy3 else 'rU' 179 with io.open(filename, mode, encoding='utf8') as f: 180 self.document.wipe() 181 self.interface.To('/') 182 oldfile = self.globals['__file__'] 183 self.globals['__file__'] = os.path.abspath(filename) 184 185 self.interface.importpath.append( 186 os.path.dirname(os.path.abspath(filename))) 187 self.runFile(f) 188 self.interface.importpath.pop() 189 self.globals['__file__'] = oldfile 190 self.document.setModified() 191 self.document.setModified(False) 192 self.document.clearHistory() 193 194 def runFile(self, fileobject): 195 """ Run a file in the preserved environment.""" 196 197 # preserve output streams 198 temp_stdout = sys.stdout 199 temp_stderr = sys.stderr 200 sys.stdout = self.write_stdout 201 sys.stderr = self.write_stderr 202 203 with self.document.suspend(): 204 # actually run the code 205 try: 206 cexec(fileobject.read(), self.globals) 207 except Exception: 208 # print out the backtrace to stderr 209 info = sys.exc_info() 210 backtrace = traceback.format_exception(*info) 211 for line in backtrace: 212 sys.stderr.write(line) 213 214 # return output streams 215 sys.stdout = temp_stdout 216 sys.stderr = temp_stderr 217 218 def evaluate(self, expression): 219 """Evaluate an expression in the environment.""" 220 221 # preserve output streams 222 temp_stdout = sys.stdout 223 temp_stderr = sys.stderr 224 sys.stdout = self.write_stdout 225 sys.stderr = self.write_stderr 226 227 # actually run the code 228 try: 229 retn = eval(expression, self.globals) 230 except Exception: 231 # print out the backtrace to stderr 232 info = sys.exc_info() 233 backtrace = traceback.format_exception(*info) 234 for line in backtrace: 235 sys.stderr.write(line) 236 retn = None 237 238 # return output streams 239 sys.stdout = temp_stdout 240 sys.stderr = temp_stderr 241 242 return retn 243 244 def GPL(self): 245 """Write the GPL to the console window.""" 246 sys.stdout.write( utils.getLicense() ) 247 248 def runPickle(self, command): 249 """Run a pickled command given as arguments. 250 251 command should consist of following: 252 dumps( (name, args, namedargs) ) 253 name is name of function to execute in environment 254 args are the arguments (list) 255 namedargs are the named arguments (dict). 256 """ 257 258 name, args, namedargs = pickle.loads(command) 259 self.globals['_tmp_args0'] = args 260 self.globals['_tmp_args1'] = namedargs 261 262 #print(name, args, namedargs) 263 try: 264 retn = eval('%s(*_tmp_args0, **_tmp_args1)' % name) 265 except Exception as e: 266 # return exception picked if exception 267 retn = e 268 269 del self.globals['_tmp_args0'] 270 del self.globals['_tmp_args1'] 271 272 return pickle.dumps(retn) 273