diff --git a/fsl/fsleyes/actions/runscript.py b/fsl/fsleyes/actions/runscript.py index 17e37bca9bf6a20dc61223f66d1d9a5b15f4ab05..18637312fd9a95a66d8e4dd7ede28997a879f65a 100644 --- a/fsl/fsleyes/actions/runscript.py +++ b/fsl/fsleyes/actions/runscript.py @@ -6,6 +6,15 @@ # """This module provides the :class:`RunScriptAction` class, which allows the user to run a custom Python script. + +The following functions are used by the :class:`RunScriptAction`, and are +available for other purposes: + + .. autosummary:: + :nosignatures: + + runScript + fsleyesScriptEnvironment """ @@ -45,35 +54,47 @@ class RunScriptAction(action.Action): self.__displayCtx = displayCtx - def __doAction(self): - """Called when this :class:`.Action` is invoked. Prompts the user to - select a script file, then compiles and runs the script. + def __doAction(self, script=None): + """Called when this :class:`.Action` is invoked. If the ``script`` + argument is ``None``, the user is prompted to select a script file. + The script is then compiled and executed. """ import wx - lastDir = fslsettings.read('runScriptLastDir') + if script is None: - if lastDir is None: - lastDir = os.getcwd() + lastDir = fslsettings.read('runScriptLastDir') - msg = strings.messages[self, 'runScript'] + if lastDir is None: + lastDir = os.getcwd() - # Ask the user what script - # they want to run - dlg = wx.FileDialog(self.__frame, - message=msg, - defaultDir=lastDir, - wildcard='*.py', - style=wx.FD_OPEN) + msg = strings.messages[self, 'runScript'] - if dlg.ShowModal() != wx.ID_OK: - return + # Ask the user what script + # they want to run + dlg = wx.FileDialog(self.__frame, + message=msg, + defaultDir=lastDir, + wildcard='*.py', + style=wx.FD_OPEN) - script = dlg.GetPath() - + if dlg.ShowModal() != wx.ID_OK: + return + + script = dlg.GetPath() + + # Save the script directory for the + # next time the user is prompted + fslsettings.write('runScriptLastDir', op.dirname(script)) + + # Run the script, show an + # error if it crashes try: - self.__runScript(script) + runScript(self.__frame, + self.__overlayList, + self.__displayCtx, + script) except Exception as e: log.warning('Script ({}) could not be executed: {}'.format( @@ -91,49 +112,74 @@ class RunScriptAction(action.Action): return - # Save the script directory - # for the next time the user - # is prompted - fslsettings.write('runScriptLastDir', op.dirname(script)) - - def __runScript(self, script): - """Compiles and executes the given file, assumed to be a Python script. - An ``Error`` is raised if the script cannot be compiled or executed. - """ +def runScript(frame, overlayList, displayCtx, script): + """Compiles and executes the given file, assumed to be a Python script. + An ``Error`` is raised if the script cannot be compiled or executed. + """ - # Set up the script environment. It's - # quite difficult to truly sand-box an - # eval'ed piece of code, but setting - # __builtins__ to None will deter simple - # attack attempts. - _globals = { - '__builtins__' : None - } - - _locals = { - 'overlayList' : self.__overlayList, - 'displayCtx' : self.__displayCtx, - 'frame' : self.__frame, - 'viewPanels' : self.__frame.getViewPanels() - } - - # We want scripts to be Python3-like - flags = (futures.print_function .compiler_flag | - futures.absolute_import .compiler_flag | - futures.division .compiler_flag | - futures.unicode_literals.compiler_flag) - - # Compile the script - with open(script, 'rt') as f: - log.debug('Compiling {}...'.format(script)) - code = f.read() - code = compile(code, - script, - mode='exec', - flags=flags, - dont_inherit=True) - - # Run the script - log.debug('Running {}...'.format(script)) - eval(code, _globals, _locals) + # We want scripts to be Python3-like + flags = (futures.print_function .compiler_flag | + futures.absolute_import .compiler_flag | + futures.division .compiler_flag | + futures.unicode_literals.compiler_flag) + + # Compile the script + with open(script, 'rt') as f: + log.debug('Compiling {}...'.format(script)) + code = f.read() + code = compile(code, + script, + mode='exec', + flags=flags, + dont_inherit=True) + + _globals, _locals = fsleyesScriptEnvironment(frame, + overlayList, + displayCtx) + + # Run the script + log.debug('Running {}...'.format(script)) + eval(code, _globals, _locals) + + +def fsleyesScriptEnvironment(frame, overlayList, displayCtx): + """Creates and returns two dictionaries, to be used as the ``globals`` + and ``locals`` dictionaries when executing a custom FSLeyes python + script. + """ + + # Set up the script environment. It's + # quite difficult to truly sand-box an + # eval'ed piece of code, but restricting + # the things contained in__builtins__ + # will deter simple attack attempts. + _globals = { + '__builtins__' : { + 'True' : True, + 'False' : False + }, + } + + import fsl.fsleyes.views as views + import fsl.fsleyes.controls as controls + import fsl.data.image as image + import fsl.data.featimage as featimage + import fsl.data.melodicimage as melimage + import fsl.data.tensorimage as tensorimage + import fsl.data.model as model + + _locals = { + 'Image' : image.Image, + 'FEATImage' : featimage.FEATImage, + 'MelodicImage' : melimage.MelodicImage, + 'TensorImage' : tensorimage.TensorImage, + 'Model' : model.Model, + 'views' : views, + 'controls' : controls, + 'overlayList' : overlayList, + 'displayCtx' : displayCtx, + 'frame' : frame, + } + + return _globals, _locals diff --git a/fsl/fsleyes/frame.py b/fsl/fsleyes/frame.py index d71d3850d2af1acf61ff00072cba6cc9ef6e95db..c4921a49995cb51d3bc3c42802c06449cbce0a56 100644 --- a/fsl/fsleyes/frame.py +++ b/fsl/fsleyes/frame.py @@ -404,9 +404,13 @@ class FSLEyesFrame(wx.Frame): self.__makePerspectiveMenu() - def runScript(self): + def runScript(self, script=None): """Runs a custom python script, via a :class:`.RunScriptAction`. """ - actions.RunScriptAction(self.__overlayList, self.__displayCtx, self)() + + rsa = actions.RunScriptAction(self.__overlayList, + self.__displayCtx, + self) + rsa(script) def __addViewPanelMenu(self, panel, title): diff --git a/fsl/fsleyes/views/shellpanel.py b/fsl/fsleyes/views/shellpanel.py index fc145d9fe70016b7f84cc92161174d81783e3976..36152f3e0161816e5b27a43e32ee6dd7474c3419 100644 --- a/fsl/fsleyes/views/shellpanel.py +++ b/fsl/fsleyes/views/shellpanel.py @@ -15,6 +15,8 @@ import wx.py.shell as wxshell from . import viewpanel +import fsl.fsleyes.actions.runscript as runscript + class ShellPanel(viewpanel.ViewPanel): """A ``ShellPanel`` is a :class:`.ViewPanel` which contains an @@ -42,22 +44,19 @@ class ShellPanel(viewpanel.ViewPanel): """ viewpanel.ViewPanel.__init__(self, parent, overlayList, displayCtx) - lcls = { - 'displayCtx' : displayCtx, - 'overlayList' : overlayList, - 'frame' : frame, - 'viewPanel' : parent, - } + _globals, _locals = runscript.fsleyesScriptEnvironment(frame, + overlayList, + displayCtx) + shell = wxshell.Shell( self, introText=' FSLEyes python shell\n\n' - 'Available variables are:\n' + 'Available FSLeyes variables are:\n' ' - overlayList\n' ' - displayCtx\n' - ' - frame\n' - ' - viewPanel\n\n', - locals=lcls, + ' - frame\n', + locals=_locals, showInterpIntro=False) # TODO set up environment so that users can @@ -66,14 +65,12 @@ class ShellPanel(viewpanel.ViewPanel): # # - Load overlays from a URL # - # - make plots - already possible with pylab, but make - # sure it works properly (i.e. doesn't clobber the shell) + # - make plots # # - run scripts (add a 'load/run' button) # # - open/close view panels, and manipulate existing view panels # - shell.push('from pylab import *\n') font = shell.GetFont() diff --git a/fsl/tools/fsleyes.py b/fsl/tools/fsleyes.py index aedafad4b627b68ec61667bb2cc39ff0737659fe..1a119166adff2a9dae99bba369707af984d23992 100644 --- a/fsl/tools/fsleyes.py +++ b/fsl/tools/fsleyes.py @@ -66,6 +66,10 @@ def parseArgs(argv): add_help=False, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument('-r', '--runscript', + metavar='SCRIPTFILE', + help='Run custom FSLeyes script') + # TODO Dynamically generate perspective list # in description. To do this, you will need # to make fsl.utils.settings work without a @@ -78,7 +82,10 @@ def parseArgs(argv): Use the '--scene' option to load a saved perspective (e.g. 'default', 'melodic', 'feat', 'ortho', or 'lightbox'). - If no '--scene' is specified, the previous layout is restored. + If no '--scene' is specified, the previous layout is restored, unless + a script is provided via the '--runscript' argument, in which case + it is assumed that the script sets up the scene, so the previous + layout is not restored. """) # Options for configuring the scene are @@ -86,7 +93,8 @@ def parseArgs(argv): return fsleyes_parseargs.parseArgs(parser, argv, name, - description) + description, + fileOpts=['r', 'runscript']) def context(args, splash): @@ -205,6 +213,7 @@ def interface(parent, args, ctx): overlayList, displayCtx, splashFrame = ctx + # Set up the frame scene (a.k.a. layout, perspective) # The scene argument can be: # # - 'lightbox' or 'ortho', specifying a single view @@ -212,13 +221,16 @@ def interface(parent, args, ctx): # # - The name of a saved (or built-in) perspective # - # - None, in which case the previous layout is restored - scene = args.scene + # - None, in which case the previous layout is restored, + # unless a custom script has been provided. + script = args.runscript + scene = args.scene - # If a scene or perspective has not been - # specified, the default behaviour is to - # restore the previous frame layout. - restore = scene is None + # If a scene/perspective or custom script + # has not been specified, the default + # behaviour is to restore the previous + # frame layout. + restore = (scene is None) and (script is None) status.update('Creating FSLeyes interface...') @@ -253,17 +265,15 @@ def interface(parent, args, ctx): else: wx.CallLater(250, splashFrame.Close) + status.update('Setting up scene...') + # If a perspective has been specified, # we load the perspective if args.scene is not None: perspectives.loadPerspective(frame, args.scene) - # The viewPanel is assumed to be a CanvasPanel - # (i.e. either OrthoPanel or LightBoxPanel) + # Apply any view-panel specific arguments viewPanels = frame.getViewPanels() - - status.update('Setting up scene...') - for viewPanel in viewPanels: if not isinstance(viewPanel, views.CanvasPanel): @@ -286,6 +296,15 @@ def interface(parent, args, ctx): if isinstance(viewPanel, views.OrthoPanel): async.idle(centre) + + # If a script has been specified, we run + # the script. This has to be done on the + # idle loop, because overlays specified + # on the command line are loaded on the + # idle loop, and the script may assume + # that they have already been loaded. + if script is not None: + async.idle(frame.runScript, script) return frame