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