Skip to content
Snippets Groups Projects
Commit 5d7bfe4d authored by Paul McCarthy's avatar Paul McCarthy
Browse files

A custom FSLeyes script can now be specified on the command line. The

ShellPanel uses the same environment as used by the RunScriptAction
parent e147535b
No related branches found
No related tags found
No related merge requests found
......@@ -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
......@@ -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):
......
......@@ -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()
......
......@@ -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
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment