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