Fix a pickling problems with hooke.plugin.playlist.FilterCommand.
[hooke.git] / hooke / plugin / playlist.py
1 # Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
2 #
3 # This file is part of Hooke.
4 #
5 # Hooke is free software: you can redistribute it and/or
6 # modify it under the terms of the GNU Lesser General Public
7 # License as published by the Free Software Foundation, either
8 # version 3 of the License, or (at your option) any later version.
9 #
10 # Hooke is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU Lesser General Public License for more details.
14 #
15 # You should have received a copy of the GNU Lesser General Public
16 # License along with Hooke.  If not, see
17 # <http://www.gnu.org/licenses/>.
18
19 """The ``playlist`` module provides :class:`PlaylistPlugin` and
20 several associated :class:`hooke.command.Command`\s for handling
21 :mod:`hooke.playlist` classes.
22 """
23
24 import glob
25
26 from ..command import Command, Argument, Failure
27 from ..playlist import FilePlaylist
28 from ..plugin import Builtin
29
30
31 class PlaylistPlugin (Builtin):
32     def __init__(self):
33         super(PlaylistPlugin, self).__init__(name='playlist')
34         self._commands = [
35             NextCommand(self), PreviousCommand(self), JumpCommand(self),
36             IndexCommand(self), CurveListCommand(self),
37             SaveCommand(self), LoadCommand(self),
38             AddCommand(self), AddGlobCommand(self),
39             RemoveCommand(self), FilterCommand(self), NoteFilterCommand(self)]
40
41
42 # Define common or complicated arguments
43
44 def current_playlist_callback(hooke, command, argument, value):
45     if value != None:
46         return value
47     playlist = hooke.playlists.current()
48     if playlist == None:
49         raise Failure('No playlists loaded')
50     return playlist
51
52 PlaylistArgument = Argument(
53     name='playlist', type='playlist', callback=current_playlist_callback,
54     help="""
55 :class:`hooke.playlist.Playlist` to act on.  Defaults to the current
56 playlist.
57 """.strip())
58
59 def playlist_name_callback(hooke, command, argument, value):
60         return hooke.playlists.free_name()
61
62 PlaylistNameArgument = Argument(
63     name='name', type='string', optional=True, callback=playlist_name_callback,
64     help="""
65 Name of the new playlist (defaults to an auto-generated name).
66 """.strip())
67
68 def all_drivers_callback(hooke, command, argument, value):
69     return hooke.drivers
70
71
72 # Define commands
73
74 class NextCommand (Command):
75     """Move playlist to the next curve.
76     """
77     def __init__(self, plugin):
78         super(NextCommand, self).__init__(
79             name='next curve',
80             arguments=[PlaylistArgument],
81             help=self.__doc__, plugin=plugin)
82
83     def _run(self, hooke, inqueue, outqueue, params):
84         params['playlist'].next()
85
86 class PreviousCommand (Command):
87     """Move playlist to the previous curve.
88     """
89     def __init__(self, plugin):
90         super(PreviousCommand, self).__init__(
91             name='previous curve',
92             arguments=[PlaylistArgument],
93             help=self.__doc__, plugin=plugin)
94
95     def _run(self, hooke, inqueue, outqueue, params):
96         params['playlist'].previous()
97
98 class JumpCommand (Command):
99     """Move playlist to a given curve.
100     """
101     def __init__(self, plugin):
102         super(JumpCommand, self).__init__(
103             name='jump to curve',
104             arguments=[
105                 PlaylistArgument,
106                 Argument(name='index', type='int', optional=False, help="""
107 Index of target curve.
108 """.strip()),
109                 ],
110             help=self.__doc__, plugin=plugin)
111
112     def _run(self, hooke, inqueue, outqueue, params):
113         params['playlist'].jump(params['index'])
114
115 class IndexCommand (Command):
116     """Print the index of the current curve.
117
118     The first curve has index 0.
119     """
120     def __init__(self, plugin):
121         super(IndexCommand, self).__init__(
122             name='curve index',
123             arguments=[
124                 PlaylistArgument,
125                 ],
126             help=self.__doc__, plugin=plugin)
127
128     def _run(self, hooke, inqueue, outqueue, params):
129         outqueue.put(params['playlist']._index)
130
131 class CurveListCommand (Command):
132     """Get the curves in a playlist.
133     """
134     def __init__(self, plugin):
135         super(CurveListCommand, self).__init__(
136             name='playlist curves',
137             arguments=[
138                 PlaylistArgument,
139                 ],
140             help=self.__doc__, plugin=plugin)
141
142     def _run(self, hooke, inqueue, outqueue, params):
143         outqueue.put([c for c in params['playlist']])
144
145 class SaveCommand (Command):
146     """Save a playlist.
147     """
148     def __init__(self, plugin):
149         super(SaveCommand, self).__init__(
150             name='save playlist',
151             arguments=[
152                 PlaylistArgument,
153                 Argument(name='output', type='file',
154                          help="""
155 File name for the output playlist.  Defaults to overwriting the input
156 playlist.
157 """.strip()),
158                 ],
159             help=self.__doc__, plugin=plugin)
160
161     def _run(self, hooke, inqueue, outqueue, params):
162         params['playlist'].save(params['output'])
163
164 class LoadCommand (Command):
165     """Load a playlist.
166     """
167     def __init__(self, plugin):
168         super(LoadCommand, self).__init__(
169             name='load playlist',
170             arguments=[
171                 Argument(name='input', type='file', optional=False,
172                          help="""
173 File name for the input playlist.
174 """.strip()),
175                 Argument(name='drivers', type='driver', optional=True,
176                          count=-1, callback=all_drivers_callback,
177                          help="""
178 Drivers for loading curves.
179 """.strip()),
180                 ],
181             help=self.__doc__, plugin=plugin)
182
183     def _run(self, hooke, inqueue, outqueue, params):
184         p = FilePlaylist(drivers=params['drivers'], path=params['input'])
185         p.load()
186         hooke.playlists.append(p)
187         outqueue.put(p)
188
189 class AddCommand (Command):
190     """Add a curve to a playlist.
191     """
192     def __init__(self, plugin):
193         super(AddCommand, self).__init__(
194             name='add curve to playlist',
195             arguments=[
196                 PlaylistArgument,
197                 Argument(name='input', type='file', optional=False,
198                          help="""
199 File name for the input :class:`hooke.curve.Curve`.
200 """.strip()),
201                 Argument(name='info', type='dict', optional=True,
202                          help="""
203 Additional information for the input :class:`hooke.curve.Curve`.
204 """.strip()),
205                 ],
206             help=self.__doc__, plugin=plugin)
207
208     def _run(self, hooke, inqueue, outqueue, params):
209         params['playlist'].append_curve_by_path(params['input'],
210                                                 params['info'])
211
212 class AddGlobCommand (Command):
213     """Add curves to a playlist with file globbing.
214
215     Adding lots of files one at a time can be tedious.  With this
216     command you can use globs (`data/curves/*.dat`) to add curves
217     for all matching files at once.
218     """
219     def __init__(self, plugin):
220         super(AddGlobCommand, self).__init__(
221             name='glob curves to playlist',
222             arguments=[
223                 PlaylistArgument,
224                 Argument(name='input', type='glob', optional=False,
225                          help="""
226 File name glob for the input :class:`hooke.curve.Curve`.
227 """.strip()),
228                 Argument(name='info', type='dict', optional=True,
229                          help="""
230 Additional information for the input :class:`hooke.curve.Curve`.
231 """.strip()),
232                 ],
233             help=self.__doc__, plugin=plugin)
234
235     def _run(self, hooke, inqueue, outqueue, params):
236         for path in sorted(glob.glob(params['input'])):
237             params['playlist'].append_curve_by_path(path, params['info'])
238
239 class RemoveCommand (Command):
240     """Remove a curve from a playlist.
241     """
242     def __init__(self, plugin):
243         super(RemoveCommand, self).__init__(
244             name='remove curve from playlist',
245             arguments=[
246                 PlaylistArgument,
247                 Argument(name='index', type='int', optional=False, help="""
248 Index of target curve.
249 """.strip()),
250                 ],
251             help=self.__doc__, plugin=plugin)
252
253     def _run(self, hooke, inqueue, outqueue, params):
254         params['playlist'].pop(params['index'])
255         params['playlist'].jump(params._index)
256
257 class FilterCommand (Command):
258     """Create a subset playlist via a selection function.
259
260     Removing lots of curves one at a time can be tedious.  With this
261     command you can use a function `filter` to select the curves you
262     wish to keep.
263
264     Notes
265     -----
266     There are issues with pickling functions bound to class
267     attributes, because the pickle module doesn't know where those
268     functions were originally defined (where it should point the
269     loader).  Because of this, subclasses with hard-coded filter
270     functions are encouraged to define their filter function as a
271     method of their subclass.  See, for example,
272     :meth:`NoteFilterCommand.filter`.
273     """
274     def __init__(self, plugin, name='filter playlist'):
275         super(FilterCommand, self).__init__(
276             name=name,
277             arguments=[
278                 PlaylistArgument,
279                 PlaylistNameArgument,
280                 ],
281             help=self.__doc__, plugin=plugin)
282         if not hasattr(self, 'filter'):
283             self.arguments.append(
284                 Argument(name='filter', type='function', optional=False,
285                          help="""
286 Function returning `True` for "good" curves.
287 `filter(curve, hooke, inqueue, outqueue, params) -> True/False`.
288 """.strip()))
289
290     def _run(self, hooke, inqueue, outqueue, params):
291         if not hasattr(self, 'filter'):
292             filter_fn = params['filter']
293         else:
294             filter_fn = self.filter
295         p = params['playlist'].filter(filter_fn,
296             hooke=hooke, inqueue=inqueue, outqueue=outqueue, params=params)
297         hooke.playlists.add(p)
298         outqueue.put(p)
299
300 class NoteFilterCommand (FilterCommand):
301     """Create a subset playlist of curves with `.info['note'] != None`.
302     """
303     def __init__(self, plugin):
304         super(NoteFilterCommand, self).__init__(
305             plugin, name='note filter playlist')
306
307     def filter(self, curve, hooke, inqueue, outqueue, params):
308         return 'note' in curve.info and curve.info['note'] != None