4dd32b7406c2db9a8f37240173fc7ea0b86ae0ab
[hooke.git] / hooke / plugin / tutorial.py
1 # Copyright (C) 2007-2011 Massimo Sandal <devicerandom@gmail.com>
2 #                         W. Trevor King <wking@drexel.edu>
3 #
4 # This file is part of Hooke.
5 #
6 # Hooke is free software: you can redistribute it and/or modify it
7 # under the terms of the GNU Lesser General Public License as
8 # published by the Free Software Foundation, either version 3 of the
9 # License, or (at your option) any later version.
10 #
11 # Hooke is distributed in the hope that it will be useful, but WITHOUT
12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 # or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General
14 # Public License for more details.
15 #
16 # You should have received a copy of the GNU Lesser General Public
17 # License along with Hooke.  If not, see
18 # <http://www.gnu.org/licenses/>.
19
20 """This plugin contains example commands to teach how to write an
21 Hooke plugin, including description of main Hooke internals.
22 """
23
24 import logging
25 import StringIO
26 import sys
27
28 from numpy import arange
29
30 from ..command import Command, Argument, Failure
31 from ..config import Setting
32 from ..interaction import PointRequest, PointResponse
33 from ..util.si import ppSI, split_data_label
34 from . import Plugin
35 from .curve import CurveArgument
36
37
38 class TutorialPlugin (Plugin):
39     """An example plugin explaining how to code plugins.
40
41     Unlike previous versions of Hooke, the class name is no longer
42     important.  Plugins identify themselves to
43     :func:`hooke.util.pluggable.construct_graph` by being subclasses
44     of :class:`hooke.plugin.Plugin`.  However, for consistency we
45     suggest the following naming scheme, show here for the 'tutorial'
46     plugin:
47
48     ===========  ==============
49     module file  tutorial.py
50     class name   TutorialPlugin
51     .name        'tutorial'
52     ===========  ==============
53
54     To ensure filename sanity,
55     :func:`hooke.util.pluggable.construct_graph` requires that
56     :attr:`name` does match the submodule name, but don't worry,
57     you'll get a clear exception message if you make a mistake.
58     """
59     def __init__(self):
60         """TutorialPlugin initialization code.
61
62         We call our base class' :meth:`__init__` and setup
63         :attr:`_commands`.
64         """
65         # This is the plugin initialization.  When Hooke starts and
66         # the plugin is loaded, this function is executed.  If there
67         # is something you need to do when Hooke starts, code it in
68         # this function.
69         sys.stderr.write('I am the Tutorial plugin initialization!\n')
70
71         # This super() call similar to the old-style
72         #   Plugin.__init__
73         # but super() is more robust under multiple inheritance.
74         # See Guido's introduction:
75         #   http://www.python.org/download/releases/2.2.3/descrintro/#cooperation
76         # And the related PEPs:
77         #   http://www.python.org/dev/peps/pep-0253/
78         #   http://www.python.org/dev/peps/pep-3135/
79         super(TutorialPlugin, self).__init__(name='tutorial')
80
81         # We want :meth:`commands` to return a list of
82         # :class:`hooke.command.Command` instances.  Rather than
83         # instantiate the classes for each call to :meth:`commands`,
84         # we instantiate them in a list here, and rely on
85         # :meth:`hooke.plugin.Plugin.commands` to return copies of
86         # that list.
87         self._commands = [DoNothingCommand(self), HookeInfoCommand(self),
88                           PointInfoCommand(self),]
89
90     def dependencies(self):
91         """Return a list  of names of :class:`hooke.plugin.Plugin`\s we
92         require.
93
94         Some plugins use features from other plugins.  Hooke makes sure that
95         plugins are configured in topological order and that no plugin is
96         enabled if it is missing dependencies.
97         """
98         return ['vclamp']
99
100     def default_settings(self):
101         """Return a list of :class:`hooke.config.Setting`\s for any
102         configurable plugin settings.
103
104         The suggested section setting is::
105
106             Setting(section=self.setting_section, help=self.__doc__)
107
108         You only need to worry about this if your plugin has some
109         "magic numbers" that the user may want to tweak, but that
110         won't be changing on a per-command basis.
111
112         You should lead off the list of settings with the suggested
113         section setting mentioned above.
114         """
115         return [
116             # We disable help wrapping, since we've wrapped
117             # TutorialPlugin.__doc__ ourselves, and it's more than one
118             # paragraph (textwrap.fill, used in
119             # :meth:`hooke.config.Setting.write` only handles one
120             # paragraph at a time).
121             Setting(section=self.setting_section, help=self.__doc__,
122                     wrap=False),
123             Setting(section=self.setting_section, option='favorite color',
124                     value='orange', help='Your personal favorite color.'),
125             ]
126
127
128 # Define common or complicated arguments
129
130 # Often, several commands in a plugin will use similar arguments.  For
131 # example, many curves in the 'playlist' plugin need a playlist to act
132 # on.  Rather than repeating an argument definition in several times,
133 # you can keep your code DRY (Don't Repeat Yourself) by defining the
134 # argument at the module level and referencing it during each command
135 # initialization.
136
137 def color_callback(hooke, command, argument, value):
138     """If `argument` is `None`, default to the configured 'favorite color'.
139
140     :class:`hooke.command.Argument`\s may have static defaults, but
141     for dynamic defaults, they use callback functions (like this one).
142     """
143     if value != None:
144         return value
145     return command.plugin.config['favorite color']
146
147 ColorArgument = Argument(
148     name='color', type='string', callback=color_callback,
149     help="Pick a color, any color.")
150 # See :func:`hooke.ui.gui.panel.propertyeditor.prop_from_argument` for
151 # a situation where :attr:`type` is important.
152
153
154 class DoNothingCommand (Command):
155     """This is a boring but working example of an actual Hooke command.
156     
157     As for :class:`hooke.plugin.Plugin`\s, the class name is not
158     important, but :attr:`name` is.  :attr:`name` is used (possibly
159     with some adjustment) as the name for accessing the command in the
160     various :class:`hooke.ui.UserInterface`\s.  For example the
161     `'do nothing'` command can be run from the command line UI with::
162
163        hooke> do_nothing
164
165     Note that if you now start Hooke with the command's plugin
166     activated and you type in the Hooke command line "help do_nothing"
167     you will see this very text as output. That is because we set
168     :attr:`_help` to this class' docstring on initialization.
169     """
170     def __init__(self, plugin):
171         # See the comments in TutorialPlugin.__init__ for details
172         # about super() and the docstring of
173         # :class:`hooke.command.Command` for details on the __init__()
174         # arguments.
175         super(DoNothingCommand, self).__init__(
176             name='do nothing',
177             arguments=[ColorArgument],
178             help=self.__doc__, plugin=plugin)
179
180     def _run(self, hooke, inqueue, outqueue, params):
181         """This is where the command-specific magic will happen.
182
183         If you haven't already, read the Architecture section of
184         :file:`doc/hacking.txt` (also available `online`_).  It
185         explains the engine/UI setup in more detail.
186
187         .. _online:
188           http://www.physics.drexel.edu/~wking/rsrch/hooke/hacking.html#architecture
189
190         The return value (if any) of this method is ignored.  You
191         should modify the :class:`hooke.hooke.Hooke` instance passed
192         in via `hooke` and/or return things via `outqueue`.  `inqueue`
193         is only important if your command requires mid-command user
194         interaction.
195
196         By the time this method is called, all the argument
197         preprocessing (callbacks, defaults, etc.) have already been
198         handled by :meth:`hooke.command.Command.run`.
199         """
200         # On initialization, :class:`hooke.hooke.Hooke` sets up a
201         # logger to use for Hooke-related messages.  Please use it
202         # instead of debugging 'print' calls, etc., as it is more
203         # configurable.
204         log = logging.getLogger('hooke')
205         log.debug('Watching %s paint dry' % params['color'])
206
207
208 class HookeInfoCommand (Command):
209     """Get information about the :class:`hooke.hooke.Hooke` instance.
210     """
211     def __init__(self, plugin):
212         super(HookeInfoCommand, self).__init__(
213             name='hooke info',
214             help=self.__doc__, plugin=plugin)
215
216     def _run(self, hooke, inqueue, outqueue, params):
217         outqueue.put('Hooke info:')
218         # hooke.config contains a :class:`hooke.config.HookeConfigParser`
219         # with the current hooke configuration settings.
220         config_file = StringIO.StringIO()
221         hooke.config.write(config_file)
222         outqueue.put('configuration:\n  %s'
223                      % '\n  '.join(config_file.getvalue().splitlines()))
224         # The plugin's configuration settings are also available.
225         outqueue.put('plugin config: %s' % self.plugin.config)
226         # hooke.plugins contains :class:`hooke.plugin.Plugin`\s defining
227         # :class:`hooke.command.Command`\s.
228         outqueue.put('plugins: %s'
229                      % ', '.join([plugin.name for plugin in hooke.plugins]))
230         # hooke.drivers contains :class:`hooke.driver.Driver`\s for
231         # loading curves.
232         outqueue.put('drivers: %s'
233                      % ', '.join([driver.name for driver in hooke.drivers]))
234         # hooke.playlists is a
235         # :class:`hooke.playlist.Playlists` instance full of
236         # :class:`hooke.playlist.FilePlaylist`\s.  Each playlist may
237         # contain several :class:`hooke.curve.Curve`\s representing a
238         # grouped collection of data.
239         playlist = hooke.playlists.current()
240         if playlist == None:
241             return
242         outqueue.put('current playlist: %s (%d of %d)'
243                      % (playlist.name,
244                         hooke.playlists.index(),
245                         len(hooke.playlists)))
246         curve = playlist.current()
247         if curve == None:
248             return
249         outqueue.put('current curve: %s (%d of %d)'
250                      % (curve.name,
251                         playlist.index(),
252                         len(playlist)))
253
254
255 class PointInfoCommand (Command):
256     """Get information about user-selected points.
257
258     Ordinarily a command that knew it would need user selected points
259     would declare an appropriate argument (see, for example,
260     :class:`hooke.plugin.cut.CutCommand`).  However, here we find the
261     points via user-interaction to show how user interaction works.
262     """
263     def __init__(self, plugin):
264         super(PointInfoCommand, self).__init__(
265             name='point info',
266             arguments=[
267                 CurveArgument,
268                 Argument(name='block', type='int', default=0,
269                     help="""
270 Data block that points are selected from.  For an approach/retract
271 force curve, `0` selects the approaching curve and `1` selects the
272 retracting curve.
273 """.strip()),
274                 ],
275             help=self.__doc__, plugin=plugin)
276
277     def _run(self, hooke, inqueue, outqueue, params):
278         data = params['curve'].data[params['block']]
279         while True:
280             # Ask the user to select a point.
281             outqueue.put(PointRequest(
282                     msg="Select a point",
283                     curve=params['curve'],
284                     block=params['block']))
285
286             # Get the user's response
287             result = inqueue.get()
288             if not isinstance(result, PointResponse):
289                 inqueue.put(result)  # put the message back in the queue
290                 raise Failure(
291                     'expected a PointResponse instance but got %s.'
292                     % type(result))
293             point = result.value
294
295             # Act on the response
296             if point == None:
297                 break
298             values = []
299             for column_name in data.info['columns']:
300                 name,unit = split_data_label(column_name)
301                 column_index = data.info['columns'].index(column_name)
302                 value = data[point,column_index]
303                 si_value = ppSI(value, unit, decimals=2)
304                 values.append('%s: %s' % (name, si_value))
305
306             outqueue.put('selected point %d: %s'
307                          % (point, ', '.join(values)))