From 991979f61a625a669bbbde4c9168475ad83062e8 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sun, 9 May 2010 15:43:58 -0400 Subject: [PATCH] Finished off hooke.hooke and fleshed out hooke.ui. Highlights: * New hooke.command for CommandExit (and subclasses), Command, and Argument. For the same reason as hooke.playlist: keep core functionality out of hooke.plugin, which is only for the pluggable interface to that functionality. * New hooke.engine containing CommandEngine. Not much going on here. Perhaps I will merge it into hooke.command later... * New hooke.ui.commandline stub for 'command line' UI. * Filled in hooke.ui stub. * Added UI and CommandEngine handling to hooke.hooke.Hooke. * Added a new super-local '.hooke.cfg' to hooke.config.DEFAULT_PATHS. For project-specific hooke configuration? Mostly to avoid suprising the user by clobbering ~/.hooke.cfg. * Fix expanduser bug in hooke.config.HookeConfigParser.write. * Fix super(AddCommand, self) -> super(AddGlobCommand, self) bug in hooke.plugin.playlist.AddGlobCommand.__init__. --- hooke/command.py | 190 +++++++++ hooke/config.py | 3 +- hooke/curve.py | 4 +- hooke/engine.py | 28 ++ hooke/hooke.py | 864 ++------------------------------------- hooke/plugin/__init__.py | 197 +-------- hooke/plugin/cut.py | 3 +- hooke/plugin/playlist.py | 5 +- hooke/ui/__init__.py | 112 +++++ hooke/ui/commandline.py | 7 + 10 files changed, 383 insertions(+), 1030 deletions(-) create mode 100644 hooke/command.py create mode 100644 hooke/engine.py create mode 100644 hooke/ui/__init__.py create mode 100644 hooke/ui/commandline.py diff --git a/hooke/command.py b/hooke/command.py new file mode 100644 index 0000000..5f5228d --- /dev/null +++ b/hooke/command.py @@ -0,0 +1,190 @@ +"""The `command` module provides :class:`Command`\s and +:class:`Argument`\s for defining commands. +""" + +import Queue as queue + + +class CommandExit (Exception): + def __str__(self): + return self.__class__.__name__ + +class Success (CommandExit): + pass + +class Failure (CommandExit): + pass + +class Command (object): + """One-line command description here. + + >>> c = Command(name='test', help='An example Command.') + >>> status = c.run(NullQueue(), PrintQueue(), help=True) # doctest: +REPORT_UDIFF + ITEM: + Command: test + + Arguments: + help BOOL (bool) Print a help message. + + An example Command. + ITEM: + Success + """ + def __init__(self, name, aliases=None, arguments=[], help=''): + self.name = name + if aliases == None: + aliases = [] + self.aliases = aliases + self.arguments = [ + Argument(name='help', type='bool', default=False, count=1, + callback=StoreValue(True), help='Print a help message.'), + ] + arguments + self._help = help + + def run(self, inqueue=None, outqueue=None, **kwargs): + """`Normalize inputs and handle before punting + to :meth:`_run`. + """ + if inqueue == None: + inqueue = NullQueue() + if outqueue == None: + outqueue = NullQueue() + try: + params = self.handle_arguments(inqueue, outqueue, kwargs) + if params['help'] == True: + outqueue.put(self.help()) + raise(Success()) + self._run(inqueue, outqueue, params) + except CommandExit, e: + if isinstance(e, Failure): + outqueue.put(e.message) + outqueue.put(e) + return 1 + outqueue.put(e) + return 0 + + def _run(self, inqueue, outqueue, params): + """This is where the command-specific magic will happen. + """ + pass + + def handle_arguments(self, inqueue, outqueue, params): + """Normalize and validate input parameters (:class:`Argument` values). + """ + for argument in self.arguments: + names = [argument.name] + argument.aliases + settings = [(name,v) for name,v in params.items() if name in names] + if len(settings) == 0: + if argument.optional == True or argument.count == 0: + settings = [(argument.name, argument.default)] + else: + raise Failure('Required argument %s not set.' + % argument.name) + if len(settings) > 1: + raise Failure('Multiple settings for %s:\n %s' + % (argument.name, + '\n '.join(['%s: %s' % (name,value) + for name,value in sorted(settings)]))) + name,value = settings[0] + if name != argument.name: + params.remove(name) + params[argument.name] = value + if argument.callback != None: + value = argument.callback(self, argument, value) + params[argument.name] = value + argument.validate(value) + return params + + def help(self, *args): + name_part = 'Command: %s' % self.name + if len(self.aliases) > 0: + name_part += ' (%s)' % ', '.join(self.aliases) + argument_part = ['Arguments:'] + [a.help() for a in self.arguments] + argument_part = '\n'.join(argument_part) + help_part = self._help + return '\n\n'.join([name_part, argument_part, help_part]) + +class Argument (object): + """Structured user input for :class:`Command`\s. + + TODO: ranges for `count`? + """ + def __init__(self, name, aliases=None, type='string', metavar=None, + default=None, optional=True, count=1, + completion_callback=None, callback=None, help=''): + self.name = name + if aliases == None: + aliases = [] + self.aliases = aliases + self.type = type + if metavar == None: + metavar = type.upper() + self.metavar = metavar + self.default = default + self.optional = optional + self.count = count + self.completion_callback = completion_callback + self.callback = callback + self._help = help + + def __str__(self): + return '<%s %s>' % (self.__class__.__name__, self.name) + + def __repr__(self): + return self.__str__() + + def help(self): + parts = ['%s ' % self.name] + if self.metavar != None: + parts.append('%s ' % self.metavar) + parts.extend(['(%s) ' % self.type, self._help]) + return ''.join(parts) + + def validate(self, value): + """If `value` is not appropriate, raise `ValueError`. + """ + pass # TODO: validation + + # TODO: type conversion + +# TODO: type extensions? + +# Useful callbacks + +class StoreValue (object): + def __init__(self, value): + self.value = value + def __call__(self, command, argument, fragment=None): + return self.value + +class NullQueue (queue.Queue): + """The :class:`queue.Queue` equivalent of `/dev/null`. + + This is a bottomless pit. Items go in, but never come out. + """ + def get(self, block=True, timeout=None): + """Raise queue.Empty. + + There's really no need to override the base Queue.get, but I + want to know if someone tries to read from a NullQueue. With + the default implementation they would just block silently + forever :(. + """ + raise queue.Empty + + def put(self, item, block=True, timeout=None): + """Dump an item into the void. + + Block and timeout are meaningless, because there is always a + free slot available in a bottomless pit. + """ + pass + +class PrintQueue (NullQueue): + """Debugging :class:`NullQueue` that prints items before dropping + them. + """ + def put(self, item, block=True, timeout=None): + """Print `item` and then dump it into the void. + """ + print 'ITEM:\n%s' % item diff --git a/hooke/config.py b/hooke/config.py index 7661771..21960de 100644 --- a/hooke/config.py +++ b/hooke/config.py @@ -19,6 +19,7 @@ DEFAULT_PATHS = [ '/usr/share/hooke/hooke.cfg', '/etc/hooke/hooke.cfg', '~/.hooke.cfg', + '.hooke.cfg', ] """We start with the system files, and work our way to the more specific user files, so the user can override the sysadmin who @@ -230,7 +231,7 @@ class HookeConfigParser (configparser.SafeConfigParser): """ local_fp = fp == None if local_fp: - fp = open(self._config_paths[-1], 'w') + fp = open(os.path.expanduser(self._config_paths[-1]), 'w') if self._defaults: print self._defaults self._write_setting(fp, configparser.DEFAULTSECT, diff --git a/hooke/curve.py b/hooke/curve.py index 0be10bc..2f8f14c 100644 --- a/hooke/curve.py +++ b/hooke/curve.py @@ -1,5 +1,5 @@ -"""The curve module provides :class:`Curve` and :class:`Data` for storing -force curves. +"""The `curve` module provides :class:`Curve` and :class:`Data` for +storing force curves. """ import os.path diff --git a/hooke/engine.py b/hooke/engine.py new file mode 100644 index 0000000..a03a2a4 --- /dev/null +++ b/hooke/engine.py @@ -0,0 +1,28 @@ +"""The `engine` module provides :class:`CommandEngine` for executing +:class:`hooke.command.Command`\s. +""" + +from .ui import CloseEngine, CommandMessage + +class CommandEngine (object): + def run(self, ui_to_command_queue, command_to_ui_queue): + """Get a :class:`hooke.ui.QueueMessage` from the incoming + `ui_to_command_queue` and act accordingly. + + If the message is a :class:`hooke.ui.CommandMessage` instance, + the command run may read subsequent input from + `ui_to_command_queue` (if it is an interactive command). The + command may also put assorted data into `command_to_ui_queue`. + + When the command completes, it will put a + :class:`hooke.command.CommandExit` instance into + `command_to_ui_queue`, at which point the `CommandEngine` will + be ready to receive the next :class:`hooke.ui.QueueMessage`. + """ + while True: + msg = ui_to_command_queue.get() + if isinstance(msg, CloseEngine): + break + assert isinstance(msg, CommandMessage), type(msg) + msg.command.run(ui_to_command_queue, command_to_ui_queue, + **msg.arguments) diff --git a/hooke/hooke.py b/hooke/hooke.py index 88d1203..0b05aff 100644 --- a/hooke/hooke.py +++ b/hooke/hooke.py @@ -3,39 +3,26 @@ ''' Hooke - A force spectroscopy review & analysis tool. +A free, open source data analysis platform + COPYRIGHT ''' -import Queue as queue import multiprocessing +from . import engine as engine from . import config as config_mod from . import plugin as plugin_mod from . import driver as driver_mod - -import copy -import cStringIO -import os -import os.path -import sys -import glob -import time - -#import libhooke as lh -#import drivers -#import plugins -#import hookecommands -#import hookeplaylist -#import hookepropertyeditor -#import hookeresults -#import playlist +from . import ui as ui class Hooke (object): def __init__(self, config=None, debug=0): self.debug = debug default_settings = (config_mod.DEFAULT_SETTINGS + plugin_mod.default_settings() - + driver_mod.default_settings()) + + driver_mod.default_settings() + + ui.default_settings()) if config == None: config = config_mod.HookeConfigParser( paths=config_mod.DEFAULT_PATHS, @@ -44,6 +31,8 @@ class Hooke (object): self.config = config self.load_plugins() self.load_drivers() + self.load_ui() + self.command = engine.CommandEngine() def load_plugins(self): self.plugins = plugin_mod.load_graph( @@ -56,825 +45,36 @@ class Hooke (object): self.drivers = plugin_mod.load_graph( driver_mod.DRIVER_GRAPH, self.config, include_section='drivers') + def load_ui(self): + self.ui = ui.load_ui(self.config) + def close(self): - if self.config.changed: - self.config.write() # Does not preserve original comments + self.config.write() # Does not preserve original comments - def playlist_status(self, playlist): - if playlist.has_curves(): - return '%s (%s/%s)' % (playlist.name, playlist._index + 1, - len(playlist)) - return 'The playlist %s does not contain any valid force curve data.' \ - % self.name + def run(self): + """Run Hooke's main execution loop. -# def _GetActiveCurveIndex(self): -# playlist = self.GetActivePlaylist() -# #get the selected item from the tree -# selected_item = self.panelPlaylists.PlaylistsTree.GetSelection() -# #test if a playlist or a curve was double-clicked -# if self.panelPlaylists.PlaylistsTree.ItemHasChildren(selected_item): -# return -1 -# else: -# count = 0 -# selected_item = self.panelPlaylists.PlaylistsTree.GetPrevSibling(selected_item) -# while selected_item.IsOk(): -# count += 1 -# selected_item = self.panelPlaylists.PlaylistsTree.GetPrevSibling(selected_item) -# return count -# -# def _GetActivePlaylistName(self): -# #get the selected item from the tree -# selected_item = self.panelPlaylists.PlaylistsTree.GetSelection() -# #test if a playlist or a curve was double-clicked -# if self.panelPlaylists.PlaylistsTree.ItemHasChildren(selected_item): -# playlist_item = selected_item -# else: -# #get the name of the playlist -# playlist_item = self.panelPlaylists.PlaylistsTree.GetItemParent(selected_item) -# #now we have a playlist -# return self.panelPlaylists.PlaylistsTree.GetItemText(playlist_item) -# -# def _GetPlaylistTab(self, name): -# for index, page in enumerate(self.plotNotebook._tabs._pages): -# if page.caption == name: -# return index -# return -1 -# -# def _GetUniquePlaylistName(self, name): -# playlist_name = name -# count = 1 -# while playlist_name in self.playlists: -# playlist_name = ''.join([name, str(count)]) -# count += 1 -# return playlist_name -# -# def _SavePerspectiveToFile(self, name, perspective): -# filename = ''.join([name, '.txt']) -# filename = lh.get_file_path(filename, ['perspectives']) -# perspectivesFile = open(filename, 'w') -# perspectivesFile.write(perspective) -# perspectivesFile.close() -# -# def AddPlaylist(self, playlist=None, name='Untitled'): -# #TODO: change cursor or progressbar (maybe in statusbar) -# #self.SetCursor(wx.StockCursor(wx.CURSOR_ARROW)) -# if playlist and playlist.count > 0: -# playlist.name = self._GetUniquePlaylistName(name) -# playlist.reset() -# self.AddToPlaylists(playlist) -# -# def AddPlaylistFromFiles(self, files=[], name='Untitled'): -# #TODO: change cursor or progressbar (maybe in statusbar) -# #self.SetCursor(wx.StockCursor(wx.CURSOR_ARROW)) -# if files: -# playlist = Playlist.Playlist(self.drivers) -# for item in files: -# playlist.append_curve_by_path(item) -# if playlist.count > 0: -# playlist.name = self._GetUniquePlaylistName(name) -# playlist.reset() -# self.AddToPlaylists(playlist) -# -# def AddToPlaylists(self, playlist): -# if playlist.has_curves: -# #setup the playlist in the Playlist tree -# tree_root = self.panelPlaylists.PlaylistsTree.GetRootItem() -# playlist_root = self.panelPlaylists.PlaylistsTree.AppendItem(tree_root, playlist.name, 0) -# #add all curves to the Playlist tree -# curves = {} -# for index, curve in enumerate(playlist.curves): -# ##remove the extension from the name of the curve -# ##TODO: optional? -# #item_text, extension = os.path.splitext(curve.name) -# #curve_ID = self.panelPlaylists.PlaylistsTree.AppendItem(playlist_root, item_text, 1) -# curve_ID = self.panelPlaylists.PlaylistsTree.AppendItem(playlist_root, curve.name, 1) -# if index == playlist.index: -# self.panelPlaylists.PlaylistsTree.SelectItem(curve_ID) -# playlist.reset() -# #create the plot tab and add playlist to the dictionary -# plotPanel = wxmpl.PlotPanel(self, ID_FirstPlot + len(self.playlists)) -# notebook_tab = self.plotNotebook.AddPage(plotPanel, playlist.name, True) -# tab_index = self.plotNotebook.GetSelection() -# figure = plotPanel.get_figure() -# self.playlists[playlist.name] = [playlist, figure] -# self.panelPlaylists.PlaylistsTree.Expand(playlist_root) -# self.statusbar.SetStatusText(playlist.get_status_string(), 0) -# self.UpdatePlot() -# -# def AppendToOutput(self, text): -# self.panelOutput.AppendText(''.join([text, '\n'])) -# -# def CreateApplicationIcon(self): -# iconFile = 'resources' + os.sep + 'microscope.ico' -# icon = wx.Icon(iconFile, wx.BITMAP_TYPE_ICO) -# self.SetIcon(icon) -# -# def CreateCommandLine(self): -# return wx.TextCtrl(self, -1, '', style=wx.NO_BORDER|wx.EXPAND) -# -# def CreatePanelAssistant(self): -# panel = wx.TextCtrl(self, -1, '', wx.Point(0, 0), wx.Size(150, 90), wx.NO_BORDER|wx.TE_MULTILINE) -# panel.SetEditable(False) -# return panel -# -# def CreatePanelCommands(self): -# return hookecommands.Commands(self) -# -# def CreatePanelFolders(self): -# #set file filters -# filters = self.config['folders']['filters'] -# index = self.config['folders'].as_int('filterindex') -# #set initial directory -# folder = self.config['general']['workdir'] -# return wx.GenericDirCtrl(self, -1, dir=folder, size=(200, 250), style=wx.DIRCTRL_SHOW_FILTERS, filter=filters, defaultFilter=index) -# -# def CreatePanelOutput(self): -# return wx.TextCtrl(self, -1, '', wx.Point(0, 0), wx.Size(150, 90), wx.NO_BORDER|wx.TE_MULTILINE) -# -# def CreatePanelPlaylists(self): -# return hookeplaylist.Playlists(self) -# -# def CreatePanelProperties(self): -# return hookepropertyeditor.PropertyEditor(self) -# -# def CreatePanelResults(self): -# return hookeresults.Results(self) -# -# def CreatePanelWelcome(self): -# ctrl = wx.html.HtmlWindow(self, -1, wx.DefaultPosition, wx.Size(400, 300)) -# introStr = '

Welcome to Hooke

' + \ -# '

Features

' + \ -# '
    ' + \ -# '
  • View, annotate, measure force curves
  • ' + \ -# '
  • Worm-like chain fit of force peaks
  • ' + \ -# '
  • Automatic convolution-based filtering of empty curves
  • ' + \ -# '
  • Automatic fit and measurement of multiple force peaks
  • ' + \ -# '
  • Handles force-clamp force experiments (experimental)
  • ' + \ -# '
  • It is extensible by users by means of plugins and drivers
  • ' + \ -# '
' + \ -# '

See the DocumentationIndex for more information

' -# ctrl.SetPage(introStr) -# return ctrl -# -# def CreateMenuBar(self): -# menu_bar = wx.MenuBar() -# #file -# file_menu = wx.Menu() -# file_menu.Append(wx.ID_OPEN, '&Open playlist\tCtrl-O') -# file_menu.Append(wx.ID_SAVE, 'Save playlist\tCtrl-S') -# file_menu.AppendSeparator() -# file_menu.Append(wx.ID_EXIT, 'Exit\tCtrl-Q') -# #edit -# edit_menu = wx.Menu() -# edit_menu.Append(ID_ExportText, 'Export text...') -# edit_menu.Append(ID_ExportImage, 'Export image...') -# edit_menu.AppendSeparator(); -# edit_menu.Append(ID_Config, 'Preferences') -# #view -# view_menu = wx.Menu() -# view_menu.AppendCheckItem(ID_ViewFolders, 'Folders\tF5') -# view_menu.AppendCheckItem(ID_ViewPlaylists, 'Playlists\tF6') -# view_menu.AppendCheckItem(ID_ViewCommands, 'Commands\tF7') -# view_menu.AppendCheckItem(ID_ViewProperties, 'Properties\tF8') -# view_menu.AppendCheckItem(ID_ViewAssistant, 'Assistant\tF9') -# view_menu.AppendCheckItem(ID_ViewResults, 'Results\tF10') -# view_menu.AppendCheckItem(ID_ViewOutput, 'Output\tF11') -# #perspectives -# self._perspectives_menu = self.CreatePerspectivesMenu() -# #help -# help_menu = wx.Menu() -# help_menu.Append(wx.ID_ABOUT, 'About Hooke') -# #put it all together -# menu_bar.Append(file_menu, 'File') -# menu_bar.Append(edit_menu, 'Edit') -# menu_bar.Append(view_menu, 'View') -# menu_bar.Append(self._perspectives_menu, "Perspectives") -# menu_bar.Append(help_menu, 'Help') -# -# self.SetMenuBar(menu_bar) -# -# def CreateNotebook(self): -# # create the notebook off-window to avoid flicker -# client_size = self.GetClientSize() -# ctrl = aui.AuiNotebook(self, -1, wx.Point(client_size.x, client_size.y), wx.Size(430, 200), self._notebook_style) -# arts = [aui.AuiDefaultTabArt, aui.AuiSimpleTabArt, aui.VC71TabArt, aui.FF2TabArt, aui.VC8TabArt, aui.ChromeTabArt] -# art = arts[self._notebook_theme]() -# ctrl.SetArtProvider(art) -# #uncomment if we find a nice icon -# #page_bmp = wx.ArtProvider.GetBitmap(wx.ART_NORMAL_FILE, wx.ART_OTHER, wx.Size(16, 16)) -# ctrl.AddPage(self.CreatePanelWelcome(), "Welcome", False) -# return ctrl -# -# def CreatePerspectivesMenu(self): -# menu = wx.Menu() -# menu.Append(ID_SavePerspective, "Save Perspective") -# menu.Append(ID_DeletePerspective, "Delete Perspective") -# menu.AppendSeparator() -# #add perspectives to menubar and _perspectives -# perspectivesDirectory = os.path.join(lh.hookeDir, 'perspectives') -# if os.path.isdir(perspectivesDirectory): -# perspectiveFileNames = os.listdir(perspectivesDirectory) -# for perspectiveFilename in perspectiveFileNames: -# filename = lh.get_file_path(perspectiveFilename, ['perspectives']) -# if os.path.isfile(filename): -# perspectiveFile = open(filename, 'rU') -# perspective = perspectiveFile.readline() -# perspectiveFile.close() -# if perspective != '': -# name, extension = os.path.splitext(perspectiveFilename) -# if extension == '.txt': -# menuItem = menu.AppendRadioItem(ID_FirstPerspective + len(self._perspectives), name) -# self._perspectives[name] = [len(self._perspectives), perspective] -# if self.config['perspectives']['active'] == name: -# menuItem.Check() -# #in case there are no perspectives -# if not self._perspectives: -# perspective = self._mgr.SavePerspective() -# self.config['perspectives']['default'] = 'Default' -# self._perspectives['Default'] = [0, perspective] -# menuItem = menu.AppendRadioItem(ID_FirstPerspective, 'Default') -# menuItem.Check() -# self._SavePerspectiveToFile('Default', perspective) -# return menu -# -# def CreateStatusbar(self): -# statusbar = self.CreateStatusBar(2, wx.ST_SIZEGRIP) -# statusbar.SetStatusWidths([-2, -3]) -# statusbar.SetStatusText('Ready', 0) -# welcomeString=u'Welcome to Hooke (version '+__version__+', '+__release_name__+')!' -# statusbar.SetStatusText(welcomeString, 1) -# return statusbar -# -# def CreateToolBar(self): -# toolbar = wx.ToolBar(self, -1, wx.DefaultPosition, wx.DefaultSize, wx.TB_FLAT | wx.TB_NODIVIDER) -# toolbar.SetToolBitmapSize(wx.Size(16,16)) -# toolbar_bmp1 = wx.ArtProvider_GetBitmap(wx.ART_QUESTION, wx.ART_OTHER, wx.Size(16, 16)) -# toolbar_bmpOpen = wx.ArtProvider_GetBitmap(wx.ART_FILE_OPEN, wx.ART_OTHER, wx.Size(16, 16)) -# toolbar_bmpSave = wx.ArtProvider_GetBitmap(wx.ART_FILE_SAVE, wx.ART_OTHER, wx.Size(16, 16)) -# toolbar_bmpExportText = wx.ArtProvider_GetBitmap(wx.ART_NORMAL_FILE, wx.ART_OTHER, wx.Size(16, 16)) -# toolbar_bmpExportImage = wx.ArtProvider_GetBitmap(wx.ART_MISSING_IMAGE, wx.ART_OTHER, wx.Size(16, 16)) -# toolbar.AddLabelTool(101, 'Open', toolbar_bmpOpen) -# toolbar.AddLabelTool(102, 'Save', toolbar_bmpSave) -# toolbar.AddSeparator() -# toolbar.AddLabelTool(ID_ExportText, 'Export text...', toolbar_bmpExportText) -# toolbar.AddLabelTool(ID_ExportImage, 'Export image...', toolbar_bmpExportImage) -# toolbar.Realize() -# return toolbar -# -# def CreateToolBarNavigation(self): -# toolbar = wx.ToolBar(self, -1, wx.DefaultPosition, wx.DefaultSize, wx.TB_FLAT | wx.TB_NODIVIDER) -# toolbar.SetToolBitmapSize(wx.Size(16,16)) -# toolbar_bmpBack = wx.ArtProvider_GetBitmap(wx.ART_GO_BACK, wx.ART_OTHER, wx.Size(16, 16)) -# toolbar_bmpForward = wx.ArtProvider_GetBitmap(wx.ART_GO_FORWARD, wx.ART_OTHER, wx.Size(16, 16)) -# toolbar.AddLabelTool(ID_Previous, 'Previous', toolbar_bmpBack, shortHelp='Previous curve') -# toolbar.AddLabelTool(ID_Next, 'Next', toolbar_bmpForward, shortHelp='Next curve') -# toolbar.Realize() -# return toolbar -# -# def DeleteFromPlaylists(self, name): -# if name in self.playlists: -# del self.playlists[name] -# tree_root = self.panelPlaylists.PlaylistsTree.GetRootItem() -# item, cookie = self.panelPlaylists.PlaylistsTree.GetFirstChild(tree_root) -# while item.IsOk(): -# playlist_name = self.panelPlaylists.PlaylistsTree.GetItemText(item) -# if playlist_name == name: -# try: -# self.panelPlaylists.PlaylistsTree.Delete(item) -# except: -# pass -# item = self.panelPlaylists.PlaylistsTree.GetNextSibling(item) -# self.OnPlaylistsLeftDclick(None) -# -# def DeletePlotPage(self, name): -# index = self._GetPlaylistTab(name) -# plot = self.playlists[name][1] -# plot = None -# self.plotNotebook.RemovePage(index) -# self.DeleteFromPlaylists(name) -# -# def GetActiveCurve(self): -# playlist = self.GetActivePlaylist() -# if playlist is not None: -# return playlist.active_curve() -# return None -# -# def GetActivePlaylist(self): -# playlist_name = self._GetActivePlaylistName() -# if playlist_name in self.playlists: -# return self.playlists[playlist_name][0] -# return None -# -# def GetActivePlot(self): -# curve = self.GetActiveCurve() -# if curve is not None: -# return curve.plots[0] -# return None -# -# def GetDockArt(self): -# return self._mgr.GetArtProvider() -# -# def GetBoolFromConfig(self, *args): -# if len(args) == 2: -# plugin = args[0] -# section = args[0] -# key = args[1] -# elif len(args) == 3: -# plugin = args[0] -# section = args[1] -# key = args[2] -# if self.configs.has_key(plugin): -# config = self.configs[plugin] -# return config[section][key].as_bool('value') -# return None -# -# def GetFloatFromConfig(self, *args): -# if len(args) == 2: -# plugin = args[0] -# section = args[0] -# key = args[1] -# elif len(args) == 3: -# plugin = args[0] -# section = args[1] -# key = args[2] -# if self.configs.has_key(plugin): -# config = self.configs[plugin] -# return config[section][key].as_float('value') -# return None -# -# def GetIntFromConfig(self, *args): -# if len(args) == 2: -# plugin = args[0] -# section = args[0] -# key = args[1] -# elif len(args) == 3: -# plugin = args[0] -# section = args[1] -# key = args[2] -# if self.configs.has_key(plugin): -# config = self.configs[plugin] -# return config[section][key].as_int('value') -# return None -# -# def GetStringFromConfig(self, *args): -# if len(args) == 2: -# plugin = args[0] -# section = args[0] -# key = args[1] -# elif len(args) == 3: -# plugin = args[0] -# section = args[1] -# key = args[2] -# if self.configs.has_key(plugin): -# config = self.configs[plugin] -# return config[section][key]['value'] -# return None -# -# def GetPerspectiveMenuItem(self, name): -# index = self._perspectives[name][0] -# perspective_Id = ID_FirstPerspective + index -# menu_item = self.MenuBar.FindItemById(perspective_Id) -# return menu_item -# -# def HasPlotmanipulator(self, name): -# ''' -# returns True if the plotmanipulator 'name' is loaded, False otherwise -# ''' -# for plotmanipulator in self.plotmanipulators: -# if plotmanipulator[0] == name: -# return True -# return False -# -# def OnAbout(self, event): -# msg = 'Hooke\n\n'+\ -# 'A free, open source data analysis platform\n'+\ -# '(c) 2006-2008 Massimo Sandal\n\n'+\ -# '(c) 2009 Dr. Rolf Schmidt\n\n'+\ -# 'Released under the GNU GPL v2' -# dialog = wx.MessageDialog(self, msg, "About Hooke", wx.OK | wx.ICON_INFORMATION) -# dialog.ShowModal() -# dialog.Destroy() -# -# def OnClose(self, event): -# #apply changes -# self.config['main']['height'] = str(self.GetSize().GetHeight()) -# self.config['main']['left'] = str(self.GetPosition()[0]) -# self.config['main']['top'] = str(self.GetPosition()[1]) -# self.config['main']['width'] = str(self.GetSize().GetWidth()) -# # Writing the configuration file to 'hooke.ini' -# self.config.write() -# self._mgr.UnInit() -# del self._mgr -# self.Destroy() -# -# def OnDeletePerspective(self, event): -# pass -# -# def OnDirCtrlLeftDclick(self, event): -# file_path = self.panelFolders.GetPath() -# if os.path.isfile(file_path): -# if file_path.endswith('.hkp'): -# self.do_loadlist(file_path) -# else: -# pass -# event.Skip() -# -# def OnEraseBackground(self, event): -# event.Skip() -# -# def OnExecute(self, event): -# item = self.panelCommands.CommandsTree.GetSelection() -# if item.IsOk(): -# if self.panelCommands.CommandsTree.ItemHasChildren(item): -# pass -# else: -# #get the plugin -# parent = self.panelCommands.CommandsTree.GetItemParent(item) -# if not self.panelCommands.CommandsTree.ItemHasChildren(item): -# parent_text = self.panelCommands.CommandsTree.GetItemText(parent) -# item_text = self.panelCommands.CommandsTree.GetItemText(item) -# if item_text in ['genlist', 'loadlist', 'savelist']: -# property_values = self.panelProperties.GetPropertyValues() -# arg_str = '' -# for item in property_values: -# arg_str = ''.join([arg_str, item, '=r"', str(property_values[item]), '", ']) -# command = ''.join(['self.do_', item_text, '(', arg_str, ')']) -# else: -# command = ''.join(['self.do_', item_text, '()']) -# exec(command) -# pass -# -# def OnExit(self, event): -# self.Close() -# -# def OnExportImage(self, event): -# pass -# -# def OnNext(self, event): -# ''' -# NEXT -# Go to the next curve in the playlist. -# If we are at the last curve, we come back to the first. -# ----- -# Syntax: next, n -# ''' -# selected_item = self.panelPlaylists.PlaylistsTree.GetSelection() -# if self.panelPlaylists.PlaylistsTree.ItemHasChildren(selected_item): -# #GetFirstChild returns a tuple -# #we only need the first element -# next_item = self.panelPlaylists.PlaylistsTree.GetFirstChild(selected_item)[0] -# else: -# next_item = self.panelPlaylists.PlaylistsTree.GetNextSibling(selected_item) -# if not next_item.IsOk(): -# parent_item = self.panelPlaylists.PlaylistsTree.GetItemParent(selected_item) -# #GetFirstChild returns a tuple -# #we only need the first element -# next_item = self.panelPlaylists.PlaylistsTree.GetFirstChild(parent_item)[0] -# self.panelPlaylists.PlaylistsTree.SelectItem(next_item, True) -# playlist = self.playlists[self._GetActivePlaylistName()][0] -# if playlist.count > 1: -# playlist.next() -# self.statusbar.SetStatusText(playlist.get_status_string(), 0) -# self.UpdatePlot() -# -# def OnNotebookPageClose(self, event): -# ctrl = event.GetEventObject() -# playlist_name = ctrl.GetPageText(ctrl._curpage) -# self.DeleteFromPlaylists(playlist_name) -# -# def OnPaneClose(self, event): -# event.Skip() -# -# def OnPlaylistsLeftDclick(self, event): -# playlist_name = self._GetActivePlaylistName() -# #if that playlist already exists -# #we check if it is the active playlist (ie selected in panelPlaylists) -# #and switch to it if necessary -# if playlist_name in self.playlists: -# index = self.plotNotebook.GetSelection() -# current_playlist = self.plotNotebook.GetPageText(index) -# #new_playlist = self.playlists[playlist_name][0] -# #if current_playlist != new_playlist: -# if current_playlist != playlist_name: -# index = self._GetPlaylistTab(playlist_name) -# self.plotNotebook.SetSelection(index) -# #if a curve was double-clicked -# item = self.panelPlaylists.PlaylistsTree.GetSelection() -# #TODO: fix with active_curve -# if not self.panelPlaylists.PlaylistsTree.ItemHasChildren(item): -# index = self._GetActiveCurveIndex() -# else: -# index = 0 -# if index >= 0: -# playlist = self.playlists[playlist_name][0] -# playlist.index = index -# self.statusbar.SetStatusText(playlist.get_status_string(), 0) -# self.UpdatePlot() -# #if you uncomment the following line, the tree will collapse/expand as well -# #event.Skip() -# -# def OnPlaylistsLeftDown(self, event): -# hit_item, hit_flags = self.panelPlaylists.PlaylistsTree.HitTest(event.GetPosition()) -# if (hit_flags & wx.TREE_HITTEST_ONITEM) != 0: -# #self.SetFocus() -# self.panelPlaylists.PlaylistsTree.SelectItem(hit_item) -# playlist_name = self._GetActivePlaylistName() -# playlist = self.playlists[playlist_name][0] -# #if a curve was clicked -# item = self.panelPlaylists.PlaylistsTree.GetSelection() -# if not self.panelPlaylists.PlaylistsTree.ItemHasChildren(item): -# #TODO: fix with active_curve -# index = self._GetActiveCurveIndex() -# if index >= 0: -# #playlist = self.playlists[playlist_name][0] -# playlist.index = index -# #self.playlists[playlist_name][0].index = index -# #else: -# ##self.playlists[playlist_name][0].index = 0 -# #playlist.index = index -# self.playlists[playlist_name][0] = playlist -# event.Skip() -# -# def OnPrevious(self, event): -# ''' -# PREVIOUS -# Go to the previous curve in the playlist. -# If we are at the first curve, we jump to the last. -# ------- -# Syntax: previous, p -# ''' -# #playlist = self.playlists[self._GetActivePlaylistName()][0] -# #select the previous curve and tell the user if we wrapped around -# #self.AppendToOutput(playlist.previous()) -# selected_item = self.panelPlaylists.PlaylistsTree.GetSelection() -# if self.panelPlaylists.PlaylistsTree.ItemHasChildren(selected_item): -# previous_item = self.panelPlaylists.PlaylistsTree.GetLastChild(selected_item) -# else: -# previous_item = self.panelPlaylists.PlaylistsTree.GetPrevSibling(selected_item) -# if not previous_item.IsOk(): -# parent_item = self.panelPlaylists.PlaylistsTree.GetItemParent(selected_item) -# previous_item = self.panelPlaylists.PlaylistsTree.GetLastChild(parent_item) -# self.panelPlaylists.PlaylistsTree.SelectItem(previous_item, True) -# playlist = self.playlists[self._GetActivePlaylistName()][0] -# if playlist.count > 1: -# playlist.previous() -# self.statusbar.SetStatusText(playlist.get_status_string(), 0) -# self.UpdatePlot() -# -# def OnPropGridChanged (self, event): -# prop = event.GetProperty() -# if prop: -# item_section = self.panelProperties.SelectedTreeItem -# item_plugin = self.panelCommands.CommandsTree.GetItemParent(item_section) -# plugin = self.panelCommands.CommandsTree.GetItemText(item_plugin) -# config = self.configs[plugin] -# property_section = self.panelCommands.CommandsTree.GetItemText(item_section) -# property_key = prop.GetName() -# property_value = prop.GetValue() -# config[property_section][property_key]['value'] = property_value -# -# def OnPropGridSelect(self, event): -# pass -# -# def OnRestorePerspective(self, event): -# name = self.MenuBar.FindItemById(event.GetId()).GetLabel() -# self._mgr.LoadPerspective(self._perspectives[name][1]) -# self.config['perspectives']['active'] = name -# self._mgr.Update() -# all_panes = self._mgr.GetAllPanes() -# for pane in all_panes: -# if not pane.name.startswith('toolbar'): -# if pane.name == 'Assistant': -# self.MenuBar.FindItemById(ID_ViewAssistant).Check(pane.window.IsShown()) -# if pane.name == 'Folders': -# self.MenuBar.FindItemById(ID_ViewFolders).Check(pane.window.IsShown()) -# if pane.name == 'Playlists': -# self.MenuBar.FindItemById(ID_ViewPlaylists).Check(pane.window.IsShown()) -# if pane.name == 'Commands': -# self.MenuBar.FindItemById(ID_ViewCommands).Check(pane.window.IsShown()) -# if pane.name == 'Properties': -# self.MenuBar.FindItemById(ID_ViewProperties).Check(pane.window.IsShown()) -# if pane.name == 'Output': -# self.MenuBar.FindItemById(ID_ViewOutput).Check(pane.window.IsShown()) -# if pane.name == 'Results': -# self.MenuBar.FindItemById(ID_ViewResults).Check(pane.window.IsShown()) -# -# def OnResultsCheck(self, index, flag): -# curve = self.GetActiveCurve() -# result = curve.data['results'][index]['visible'] = flag -# self.UpdatePlot() -# -# def OnSavePerspective(self, event): -# def nameExists(name): -# for item in self._perspectives_menu.GetMenuItems(): -# if item.GetText() == name: -# return True -# return False -# -# done = False -# while not done: -# dialog = wx.TextEntryDialog(self, 'Enter a name for the new perspective:', 'Save perspective') -# dialog.SetValue('New perspective') -# if dialog.ShowModal() != wx.ID_OK: -# return -# else: -# name = dialog.GetValue() -# -# if nameExists(name): -# dialogConfirm = wx.MessageDialog(self, 'A file with this name already exists.\n\nDo you want to replace it?', 'Confirm', wx.YES_NO|wx.ICON_QUESTION|wx.CENTER) -# if dialogConfirm.ShowModal() == wx.ID_YES: -# done = True -# else: -# done = True -# -# perspective = self._mgr.SavePerspective() -# -# if nameExists(name): -# #check the corresponding menu item -# menuItem = self.GetPerspectiveMenuItem(name) -# #replace the perspectiveStr in _pespectives -# index = self._perspectives[name][0] -# self._perspectives[name] = [index, perspective] -# else: -# #simply add the new perspective to _perspectives -# index = len(self._perspectives) -# self._perspectives[name] = [len(self._perspectives), perspective] -# menuItem = self._perspectives_menu.AppendRadioItem(ID_FirstPerspective + len(self._perspectives), name) -# -# menuItem.Check() -# self._SavePerspectiveToFile(name, perspective) -# #uncheck all perspective menu items -# #as these are radio items, one item has to be checked at all times -# #the line 'menuItem.Check()' above actually checks a second item -# #but does not toggle -# #so we need to uncheck all other items afterwards -# #weirdly enough, menuitem.Toggle() doesn't do this properly either -# for item in self._perspectives_menu.GetMenuItems(): -# if item.IsCheckable(): -# if item.GetLabel() != name: -# item.Check(False) -# -# def OnView(self, event): -# menu_id = event.GetId() -# menu_item = self.MenuBar.FindItemById(menu_id) -# menu_label = menu_item.GetLabel() -# -# pane = self._mgr.GetPane(menu_label) -# pane.Show(not pane.IsShown()) -# #if we don't do the following, the Folders pane does not resize properly on hide/show -# if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked(): -# #folders_size = pane.GetSize() -# self.panelFolders.Fit() -# self._mgr.Update() -# -# def OnSize(self, event): -# event.Skip() -# -# def OnTreeCtrlCommandsLeftDown(self, event): -# hit_item, hit_flags = self.panelCommands.CommandsTree.HitTest(event.GetPosition()) -# if (hit_flags & wx.TREE_HITTEST_ONITEM) != 0: -# self.panelCommands.CommandsTree.SelectItem(hit_item) -# self.panelProperties.SelectedTreeItem = hit_item -# #if a command was clicked -# properties = [] -# if not self.panelCommands.CommandsTree.ItemHasChildren(hit_item): -# item_plugin = self.panelCommands.CommandsTree.GetItemParent(hit_item) -# plugin = self.panelCommands.CommandsTree.GetItemText(item_plugin) -# if self.configs.has_key(plugin): -# #config = self.panelCommands.CommandsTree.GetPyData(item_plugin) -# config = self.configs[plugin] -# section = self.panelCommands.CommandsTree.GetItemText(hit_item) -# #display docstring in help window -# doc_string = eval('self.do_' + section + '.__doc__') -# if section in config: -# for option in config[section]: -# properties.append([option, config[section][option]]) -# else: -# module = self.panelCommands.CommandsTree.GetItemText(hit_item) -# if module != 'general': -# doc_string = eval('plugins.' + module + '.' + module + 'Commands.__doc__') -# else: -# doc_string = 'The module "general" contains Hooke core functionality' -# if doc_string is not None: -# self.panelAssistant.ChangeValue(doc_string) -# hookepropertyeditor.PropertyEditor.Initialize(self.panelProperties, properties) -# event.Skip() -# -# def UpdatePlaylists(self): -# #setup the playlist in the Playlist tree -# tree_root = self.panelPlaylists.PlaylistsTree.GetRootItem() -# playlist_root = self.panelPlaylists.PlaylistsTree.AppendItem(tree_root, playlist.name, 0) -# #add all curves to the Playlist tree -# curves = {} -# for index, curve in enumerate(playlist.curves): -# ##remove the extension from the name of the curve -# ##TODO: optional? -# #item_text, extension = os.path.splitext(curve.name) -# #curve_ID = self.panelPlaylists.PlaylistsTree.AppendItem(playlist_root, item_text, 1) -# curve_ID = self.panelPlaylists.PlaylistsTree.AppendItem(playlist_root, curve.name, 1) -# if index == playlist.index: -# self.panelPlaylists.PlaylistsTree.SelectItem(curve_ID) -# #create the plot tab and add playlist to the dictionary -# plotPanel = wxmpl.PlotPanel(self, ID_FirstPlot + len(self.playlists)) -# notebook_tab = self.plotNotebook.AddPage(plotPanel, playlist.name, True) -# #tab_index = self.plotNotebook.GetSelection() -# figure = plotPanel.get_figure() -# #self.playlists[playlist.name] = [playlist, tab_index, figure] -# self.playlists[playlist.name] = [playlist, figure] -# self.panelPlaylists.PlaylistsTree.Expand(playlist_root) -# self.statusbar.SetStatusText(playlist.get_status_string(), 0) -# self.UpdatePlot() -# -##HELPER FUNCTIONS -##Everything sending an event should be here -# def _measure_N_points(self, N, whatset=1): -# ''' -# general helper function for N-points measures -# ''' -# wx.PostEvent(self.frame,self.list_of_events['measure_points'](num_of_points=N, set=whatset)) -# while 1: -# try: -# points=self.frame.events_from_gui.get() -# break -# except Empty: -# pass -# return points -# -# def _clickize(self, xvector, yvector, index): -# ''' -# returns a ClickedPoint() object from an index and vectors of x, y coordinates -# ''' -# point = lh.ClickedPoint() -# point.index = index -# point.absolute_coords = xvector[index], yvector[index] -# point.find_graph_coords(xvector, yvector) -# return point -# -# -##PLOT INTERACTION COMMANDS -##------------------------------- -# def UpdatePlot(self): -# def add_plot(plot): -# if plot['visible'] and plot['x'] and plot['y']: -# color = plot['color'] -# style = plot['style'] -# if style == 'plot': -# axes.plot(plot['x'], plot['y'], color=color, zorder=1) -# if style == 'scatter': -# axes.scatter(plot['x'], plot['y'], color=color, s=0.5, zorder=2) -# -# def add_plot2(plot): -# if plot.visible and plot.x and plot.y: -# if plot.style == 'plot': -# axes.plot(plot.x, plot.y, color=plot.color, zorder=1) -# if plot.style == 'scatter': -# axes.scatter(plot.x, plot.y, color=plot.color, s=0.5, zorder=2) -# -# playlist_name = self._GetActivePlaylistName() -# index = self._GetActiveCurveIndex() -# playlist = self.playlists[playlist_name][0] -# curve = playlist.active_curve() -# plot = playlist.get_active_plot() -# figure = self.playlists[playlist_name][1] -# -# figure.clf() -# exclude = None -# if curve.data.has_key('manipulated'): -# exclude = 'raw' -# elif curve.data.has_key('raw'): -# exclude = 'manipulated' -# -# if exclude is not None: -# #TODO: what is this good for? -# if not hasattr(self, 'subplot'): -# axes = figure.add_subplot(111) -# -# axes.set_title(plot.title) -# axes.set_xlabel(plot.units[0]) -# axes.set_ylabel(plot.units[1]) -# -# for set_of_plots in curve.data: -# if set_of_plots != exclude: -# plots = curve.data[set_of_plots] -# for each_plot in plots: -# add_plot(each_plot) -# -# #TODO: add multiple results support -# #for fit in curve.fits: -# if curve.fits.has_key('wlc'): -# for plot in curve.fits['wlc'].results: -# add_plot2(plot) -# self.panelResults.DisplayResults(curve.fits['wlc']) -# else: -# self.panelResults.ClearResults() -# -# axes.figure.canvas.draw() -# else: -# self.AppendToOutput('Not able to plot.') + Spawns a :class:`hooke.engine.CommandEngine` subprocess and + then runs the UI, rejoining the `CommandEngine` process after + the UI exits. + """ + ui_to_command = multiprocessing.Queue() + command_to_ui = multiprocessing.Queue() + command = multiprocessing.Process( + target=self.command.run, args=(ui_to_command, command_to_ui)) + command.start() + try: + self.ui.run(self, ui_to_command, command_to_ui) + finally: + ui_to_command.put(ui.CloseEngine()) + command.join() def main(): app = Hooke(debug=__debug__) - app.main() + try: + app.run() + finally: + app.close() if __name__ == '__main__': main() diff --git a/hooke/plugin/__init__.py b/hooke/plugin/__init__.py index 544200a..abf1bdb 100644 --- a/hooke/plugin/__init__.py +++ b/hooke/plugin/__init__.py @@ -1,15 +1,15 @@ -"""The plugin module provides optional submodules that add new Hooke +"""The `plugin` module provides optional submodules that add new Hooke commands. All of the science happens in here. """ import ConfigParser as configparser -import Queue as queue from ..config import Setting from ..util.graph import Node, Graph + PLUGIN_MODULES = [ # ('autopeak', True), # ('curvetools', True), @@ -72,7 +72,8 @@ class Plugin (object): return [] def commands(self): - """Return a list of :class:`Commands` provided.""" + """Return a list of :class:`hooke.command.Command`\s provided. + """ return [] class Builtin (Plugin): @@ -83,193 +84,6 @@ class Builtin (Plugin): """ pass -# Commands and arguments - -class CommandExit (Exception): - def __str__(self): - return self.__class__.__name__ - -class Success (CommandExit): - pass - -class Failure (CommandExit): - pass - -class Command (object): - """One-line command description here. - - >>> c = Command(name='test', help='An example Command.') - >>> status = c.run(NullQueue(), PrintQueue(), help=True) # doctest: +REPORT_UDIFF - ITEM: - Command: test - - Arguments: - help BOOL (bool) Print a help message. - - An example Command. - ITEM: - Success - """ - def __init__(self, name, aliases=None, arguments=[], help=''): - self.name = name - if aliases == None: - aliases = [] - self.aliases = aliases - self.arguments = [ - Argument(name='help', type='bool', default=False, count=1, - callback=StoreValue(True), help='Print a help message.'), - ] + arguments - self._help = help - - def run(self, inqueue=None, outqueue=None, **kwargs): - """`Normalize inputs and handle before punting - to :meth:`_run`. - """ - if inqueue == None: - inqueue = NullQueue() - if outqueue == None: - outqueue = NullQueue() - try: - params = self.handle_arguments(inqueue, outqueue, kwargs) - if params['help'] == True: - outqueue.put(self.help()) - raise(Success()) - self._run(inqueue, outqueue, params) - except CommandExit, e: - if isinstance(e, Failure): - outqueue.put(e.message) - outqueue.put(e) - return 1 - outqueue.put(e) - return 0 - - def _run(self, inqueue, outqueue, params): - """This is where the command-specific magic will happen. - """ - pass - - def handle_arguments(self, inqueue, outqueue, params): - """Normalize and validate input parameters (:class:`Argument` values). - """ - for argument in self.arguments: - names = [argument.name] + argument.aliases - settings = [(name,v) for name,v in params.items() if name in names] - if len(settings) == 0: - if argument.optional == True or argument.count == 0: - settings = [(argument.name, argument.default)] - else: - raise Failure('Required argument %s not set.' - % argument.name) - if len(settings) > 1: - raise Failure('Multiple settings for %s:\n %s' - % (argument.name, - '\n '.join(['%s: %s' % (name,value) - for name,value in sorted(settings)]))) - name,value = settings[0] - if name != argument.name: - params.remove(name) - params[argument.name] = value - if argument.callback != None: - value = argument.callback(self, argument, value) - params[argument.name] = value - argument.validate(value) - return params - - def help(self, *args): - name_part = 'Command: %s' % self.name - if len(self.aliases) > 0: - name_part += ' (%s)' % ', '.join(self.aliases) - argument_part = ['Arguments:'] + [a.help() for a in self.arguments] - argument_part = '\n'.join(argument_part) - help_part = self._help - return '\n\n'.join([name_part, argument_part, help_part]) - -class Argument (object): - """Structured user input for :class:`Command`\s. - - TODO: ranges for `count`? - """ - def __init__(self, name, aliases=None, type='string', metavar=None, - default=None, optional=True, count=1, - completion_callback=None, callback=None, help=''): - self.name = name - if aliases == None: - aliases = [] - self.aliases = aliases - self.type = type - if metavar == None: - metavar = type.upper() - self.metavar = metavar - self.default = default - self.optional = optional - self.count = count - self.completion_callback = completion_callback - self.callback = callback - self._help = help - - def __str__(self): - return '<%s %s>' % (self.__class__.__name__, self.name) - - def __repr__(self): - return self.__str__() - - def help(self): - parts = ['%s ' % self.name] - if self.metavar != None: - parts.append('%s ' % self.metavar) - parts.extend(['(%s) ' % self.type, self._help]) - return ''.join(parts) - - def validate(self, value): - """If `value` is not appropriate, raise `ValueError`. - """ - pass # TODO: validation - - # TODO: type conversion - -# TODO: type extensions? - -# Useful callbacks - -class StoreValue (object): - def __init__(self, value): - self.value = value - def __call__(self, command, argument, fragment=None): - return self.value - -class NullQueue (queue.Queue): - """The :class:`queue.Queue` equivalent of `/dev/null`. - - This is a bottomless pit. Items go in, but never come out. - """ - def get(self, block=True, timeout=None): - """Raise queue.Empty. - - There's really no need to override the base Queue.get, but I - want to know if someone tries to read from a NullQueue. With - the default implementation they would just block silently - forever :(. - """ - raise queue.Empty - - def put(self, item, block=True, timeout=None): - """Dump an item into the void. - - Block and timeout are meaningless, because there is always a - free slot available in a bottomless pit. - """ - pass - -class PrintQueue (NullQueue): - """Debugging :class:`NullQueue` that prints items before dropping - them. - """ - def put(self, item, block=True, timeout=None): - """Print `item` and then dump it into the void. - """ - print 'ITEM:\n%s' % item - - # Construct plugin dependency graph and load plugin instances. def construct_graph(this_modname, submodnames, class_selector, @@ -383,8 +197,7 @@ def load_graph(graph, config, include_section): include = True # non-optional include (e.g. a Builtin) if include == True: try: - item.config = dict( - config.items(item.setting_section)) + item.config = dict(config.items(item.setting_section)) except configparser.NoSectionError: pass items.append(item) diff --git a/hooke/plugin/cut.py b/hooke/plugin/cut.py index 351ac18..c1d8ca9 100644 --- a/hooke/plugin/cut.py +++ b/hooke/plugin/cut.py @@ -1,7 +1,8 @@ """Defines :class:`CutPlugin` and :class:`CutCommand`. """ -from ..plugin import Plugin, Command, Argument +from ..command import Command, Argument +from ..plugin import Plugin class CutPlugin (Plugin): diff --git a/hooke/plugin/playlist.py b/hooke/plugin/playlist.py index 422b679..78ad4eb 100644 --- a/hooke/plugin/playlist.py +++ b/hooke/plugin/playlist.py @@ -5,8 +5,9 @@ classes. import glob +from ..command import Command, Argument from ..playlist import FilePlaylist -from ..plugin import Builtin, Command, Argument +from ..plugin import Builtin class PlaylistPlugin (Builtin): @@ -153,7 +154,7 @@ class AddGlobCommand (Command): for all matching files at once. """ def __init__(self): - super(AddCommand, self).__init__( + super(AddGlobCommand, self).__init__( name='glob curves to playlist', arguments=[ Argument(name='playlist', type='playlist', optional=False, diff --git a/hooke/ui/__init__.py b/hooke/ui/__init__.py new file mode 100644 index 0000000..50f6fe8 --- /dev/null +++ b/hooke/ui/__init__.py @@ -0,0 +1,112 @@ +"""The `ui` module provides :class:`UserInterface` and various subclasses. +""" + +import ConfigParser as configparser + +from ..compat.odict import odict +from ..config import Setting +from ..plugin import IsSubclass + + +USER_INTERFACE_MODULES = [ + 'commandline', + #'gui', + ] +"""List of user interface modules. TODO: autodiscovery +""" + +class QueueMessage (object): + def __str__(self): + return self.__class__.__name__ + +class CloseEngine (QueueMessage): + pass + +class CommandMessage (QueueMessage): + """A message storing a command to run, `command` should be a + :class:`hooke.command.Command` instance, and `arguments` should be + a :class:`dict` with `argname` keys and `value` values to be + passed to the command. + """ + def __init__(self, command, arguments): + self.command = command + self.arguments = arguments + +class UserInterface (object): + """A user interface to drive the :class:`hooke.engine.CommandEngine`. + """ + def __init__(self, name): + self.name = name + self.setting_section = '%s ui' % self.name + self.config = {} + + def default_settings(self): + """Return a list of :class:`hooke.config.Setting`\s for any + configurable UI settings. + + The suggested section setting is:: + + Setting(section=self.setting_section, help=self.__doc__) + """ + return [] + + def run(self, hooke, ui_to_command_queue, command_to_ui_queue): + return + + def playlist_status(self, playlist): + if playlist.has_curves(): + return '%s (%s/%s)' % (playlist.name, playlist._index + 1, + len(playlist)) + return 'The playlist %s does not contain any valid force curve data.' \ + % self.name + +def construct_odict(this_modname, submodnames, class_selector): + """Search the submodules `submodnames` of a module `this_modname` + for class objects for which `class_selector(class)` returns + `True`. These classes are instantiated and stored in the returned + :class:`hooke.compat.odict.odict` in the order in which they were + discovered. + """ + instances = odict() + for submodname in submodnames: + count = len([s for s in submodnames if s == submodname]) + assert count > 0, 'No %s entries: %s' % (submodname, submodnames) + assert count == 1, 'Multiple (%d) %s entries: %s' \ + % (count, submodname, submodnames) + this_mod = __import__(this_modname, fromlist=[submodname]) + submod = getattr(this_mod, submodname) + for objname in dir(submod): + obj = getattr(submod, objname) + if class_selector(obj): + instance = obj() + instances[instance.name] = instance + return instances + +USER_INTERFACES = construct_odict( + this_modname=__name__, + submodnames=USER_INTERFACE_MODULES, + class_selector=IsSubclass(UserInterface, blacklist=[UserInterface])) +""":class:`hooke.compat.odict.odict` of :class:`UserInterface` +instances keyed by `.name`. +""" + +def default_settings(): + settings = [Setting('UIs', help='Select the user interface (only one).')] + for i,ui in enumerate(USER_INTERFACES.values()): + help = ui.__doc__.split('\n', 1)[0] + settings.append(Setting('UIs', ui.name, str(i==0), help=help)) + # i==0 to enable the first by default + for ui in USER_INTERFACES.values(): + settings.extend(ui.default_settings()) + return settings + +def load_ui(config): + uis = [c for c,v in config.items('UIs') if v == 'True'] + assert len(uis) == 1, 'Can only select one UI, not %d: %s' % (len(uis),uis) + ui_name = uis[0] + ui = USER_INTERFACES[ui_name] + try: + ui.config = dict(config.items(ui.setting_section)) + except configparser.NoSectionError: + pass + return ui diff --git a/hooke/ui/commandline.py b/hooke/ui/commandline.py new file mode 100644 index 0000000..e1554cd --- /dev/null +++ b/hooke/ui/commandline.py @@ -0,0 +1,7 @@ +from ..ui import UserInterface + +class CommandLine (UserInterface): + """Command line interface. Simple and powerful. + """ + def __init__(self): + super(CommandLine, self).__init__(name='command line') -- 2.26.2