Run update-copyright.py.
[hooke.git] / hooke / plugin / playlist.py
1 # Copyright (C) 2010-2012 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 modify it under the
6 # terms of the GNU Lesser General Public License as published by the Free
7 # Software Foundation, either version 3 of the License, or (at your option) any
8 # later version.
9 #
10 # Hooke is distributed in the hope that it will be useful, but WITHOUT ANY
11 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12 # A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
13 # details.
14 #
15 # You should have received a copy of the GNU Lesser General Public License
16 # along with Hooke.  If not, see <http://www.gnu.org/licenses/>.
17
18 """The ``playlist`` module provides :class:`PlaylistPlugin` and
19 several associated :class:`hooke.command.Command`\s for handling
20 :mod:`hooke.playlist` classes.
21 """
22
23 import glob
24 import logging
25 import os.path
26
27 from ..command import Command, Argument, Failure
28 from ..curve import NotRecognized
29 from ..playlist import load
30 from ..util.itertools import reverse_enumerate
31 from . import Builtin
32
33
34 class PlaylistPlugin (Builtin):
35     def __init__(self):
36         super(PlaylistPlugin, self).__init__(name='playlist')
37         self._commands = [
38             NextCommand(self), PreviousCommand(self), JumpCommand(self),
39             GetCommand(self), IndexCommand(self), CurveListCommand(self),
40             NameCommand(self), SaveCommand(self), LoadCommand(self),
41             AddCommand(self), AddGlobCommand(self),
42             RemoveCommand(self), ApplyCommand(self),
43             FilterCommand(self),
44             ]
45
46
47 # Define common or complicated arguments
48
49 def current_playlist_callback(hooke, command, argument, value):
50     if value != None:
51         return value
52     playlist = hooke.playlists.current()
53     if playlist == None:
54         raise Failure('No playlists loaded')
55     return playlist
56
57 PlaylistArgument = Argument(
58     name='playlist', type='playlist', callback=current_playlist_callback,
59     help="""
60 :class:`hooke.playlist.Playlist` to act on.  Defaults to the current
61 playlist.
62 """.strip())
63
64 def playlist_name_callback(hooke, command, argument, value):
65     if value != None:
66         return value
67     i = 0
68     names = [p.name for p in hooke.playlists]
69     while True:
70         name = 'playlist-%d' % i
71         if name not in names:
72             return name
73         i += 1
74
75 PlaylistNameArgument = Argument(
76     name='output playlist', type='string', optional=True,
77     callback=playlist_name_callback,
78     help="""
79 Name of the new playlist (defaults to an auto-generated name).
80 """.strip())
81
82 def all_drivers_callback(hooke, command, argument, value):
83     return hooke.drivers
84
85
86 # Define useful command subclasses
87
88 class PlaylistCommand (Command):
89     """A :class:`~hooke.command.Command` operating on a
90     :class:`~hooke.playlist.Playlist`.
91     """
92     def __init__(self, **kwargs):
93         if 'arguments' in kwargs:
94             kwargs['arguments'].insert(0, PlaylistArgument)
95         else:
96             kwargs['arguments'] = [PlaylistArgument]
97         super(PlaylistCommand, self).__init__(**kwargs)
98
99     def _playlist(self, hooke, params):
100         """Get the selected playlist.
101
102         Notes
103         -----
104         `hooke` is intended to attach the selected playlist to the
105         local hooke instance; the returned playlist should not be
106         effected by the state of `hooke`.
107         """
108         # HACK? rely on params['playlist'] being bound to the local
109         # hooke (i.e. not a copy, as you would get by passing a
110         # playlist through the queue).  Ugh.  Stupid queues.  As an
111         # alternative, we could pass lookup information through the
112         # queue...
113         return params['playlist']
114
115
116 class PlaylistAddingCommand (Command):
117     """A :class:`~hooke.command.Command` adding a
118     :class:`~hooke.playlist.Playlist`.
119     """
120     def __init__(self, **kwargs):
121         if 'arguments' in kwargs:
122             kwargs['arguments'].insert(0, PlaylistNameArgument)
123         else:
124             kwargs['arguments'] = [PlaylistNameArgument]
125         super(PlaylistAddingCommand, self).__init__(**kwargs)
126
127     def _set_playlist(self, hooke, params, playlist):
128         """Attach a new playlist.
129         """
130         playlist_names = [p.name for p in hooke.playlists]
131         if playlist.name in playlist_names or playlist.name == None:
132             playlist.name = params['output playlist']  # HACK: override input name.  How to tell if it is callback-generated?
133         hooke.playlists.append(playlist)
134
135
136 # Define commands
137
138 class NextCommand (PlaylistCommand):
139     """Move playlist to the next curve.
140     """
141     def __init__(self, plugin):
142         super(NextCommand, self).__init__(
143             name='next curve', help=self.__doc__, plugin=plugin)
144
145     def _run(self, hooke, inqueue, outqueue, params):
146         self._playlist(hooke, params).next()
147
148
149 class PreviousCommand (PlaylistCommand):
150     """Move playlist to the previous curve.
151     """
152     def __init__(self, plugin):
153         super(PreviousCommand, self).__init__(
154             name='previous curve', help=self.__doc__, plugin=plugin)
155
156     def _run(self, hooke, inqueue, outqueue, params):
157         self._playlist(hooke, params).previous()
158
159
160 class JumpCommand (PlaylistCommand):
161     """Move playlist to a given curve.
162     """
163     def __init__(self, plugin):
164         super(JumpCommand, self).__init__(
165             name='jump to curve',
166             arguments=[
167                 Argument(name='index', type='int', optional=False, help="""
168 Index of target curve.
169 """.strip()),
170                 ],
171             help=self.__doc__, plugin=plugin)
172
173     def _run(self, hooke, inqueue, outqueue, params):
174         self._playlist(hooke, params).jump(params['index'])
175
176
177 class IndexCommand (PlaylistCommand):
178     """Print the index of the current curve.
179
180     The first curve has index 0.
181     """
182     def __init__(self, plugin):
183         super(IndexCommand, self).__init__(
184             name='curve index', help=self.__doc__, plugin=plugin)
185
186     def _run(self, hooke, inqueue, outqueue, params):
187         outqueue.put(self._playlist(hooke, params).index())
188
189
190 class GetCommand (PlaylistCommand):
191     """Return a :class:`hooke.playlist.Playlist`.
192     """
193     def __init__(self, plugin):
194         super(GetCommand, self).__init__(
195             name='get playlist', help=self.__doc__, plugin=plugin)
196
197     def _run(self, hooke, inqueue, outqueue, params):
198         outqueue.put(self._playlist(hooke, params))
199
200
201 class CurveListCommand (PlaylistCommand):
202     """Get the curves in a playlist.
203     """
204     def __init__(self, plugin):
205         super(CurveListCommand, self).__init__(
206             name='playlist curves', help=self.__doc__, plugin=plugin)
207
208     def _run(self, hooke, inqueue, outqueue, params):
209         outqueue.put(list(self._playlist(hooke, params)))
210
211
212 class NameCommand (PlaylistCommand):
213     """(Re)name a playlist.
214     """
215     def __init__(self, plugin):
216         super(NameCommand, self).__init__(
217             name='name playlist',
218             arguments=[
219                 Argument(name='name', type='string', optional=False,
220                          help="""
221 Name for the playlist.
222 """.strip()),
223                 ],
224             help=self.__doc__, plugin=plugin)
225
226     def _run(self, hooke, inqueue, outqueue, params):
227         p = self._playlist(hooke, params)
228         p.name = params['name']
229         outqueue.put(p)
230
231
232 class SaveCommand (PlaylistCommand):
233     """Save a playlist.
234     """
235     def __init__(self, plugin):
236         super(SaveCommand, self).__init__(
237             name='save playlist',
238             arguments=[
239                 Argument(name='output', type='file',
240                          help="""
241 File name for the output playlist.  Defaults to overwriting the input
242 playlist.  If the playlist does not have an input file (e.g. it was
243 created from scratch with 'new playlist'), this option is required.
244 """.strip()),
245                 ],
246             help=self.__doc__, plugin=plugin)
247
248     def _run(self, hooke, inqueue, outqueue, params):
249         self._playlist(hooke, params).save(params['output'])
250
251
252 class LoadCommand (PlaylistAddingCommand):
253     """Load a playlist.
254     """
255     def __init__(self, plugin):
256         super(LoadCommand, self).__init__(
257             name='load playlist',
258             arguments=[
259                 Argument(name='input', type='file', optional=False,
260                          help="""
261 File name for the input playlist.
262 """.strip()),
263                 Argument(name='drivers', type='driver', optional=True,
264                          count=-1, callback=all_drivers_callback,
265                          help="""
266 Drivers for loading curves.
267 """.strip()),
268                 ],
269             help=self.__doc__, plugin=plugin)
270
271     def _run(self, hooke, inqueue, outqueue, params):
272         p = load(path=params['input'], drivers=params['drivers'], hooke=hooke)
273         self._set_playlist(hooke, params, p)
274         outqueue.put(p)
275
276
277 class AddCommand (PlaylistCommand):
278     """Add a curve to a playlist.
279     """
280     def __init__(self, plugin):
281         super(AddCommand, self).__init__(
282             name='add curve to playlist',
283             arguments=[
284                 Argument(name='input', type='file', optional=False,
285                          help="""
286 File name for the input :class:`hooke.curve.Curve`.
287 """.strip()),
288                 Argument(name='info', type='dict', optional=True,
289                          help="""
290 Additional information for the input :class:`hooke.curve.Curve`.
291 """.strip()),
292                 ],
293             help=self.__doc__, plugin=plugin)
294
295     def _run(self, hooke, inqueue, outqueue, params):
296         self._playlist(hooke, params).append_curve_by_path(
297             params['input'], params['info'], hooke=hooke)
298
299
300 class AddGlobCommand (PlaylistCommand):
301     """Add curves to a playlist with file globbing.
302
303     Adding lots of files one at a time can be tedious.  With this
304     command you can use globs (`data/curves/*.dat`) to add curves
305     for all matching files at once.
306     """
307     def __init__(self, plugin):
308         super(AddGlobCommand, self).__init__(
309             name='glob curves to playlist',
310             arguments=[
311                 Argument(name='input', type='string', optional=False,
312                          help="""
313 File name glob for the input :class:`hooke.curve.Curve`.
314 """.strip()),
315                 Argument(name='info', type='dict', optional=True,
316                          help="""
317 Additional information for the input :class:`hooke.curve.Curve`.
318 """.strip()),
319                 ],
320             help=self.__doc__, plugin=plugin)
321
322     def _run(self, hooke, inqueue, outqueue, params):
323         p = self._playlist(hooke, params)
324         for path in sorted(glob.glob(os.path.expanduser(params['input']))):
325             try:
326                 p.append_curve_by_path(path, params['info'], hooke=hooke)
327             except NotRecognized, e:
328                 log = logging.getLogger('hooke')
329                 log.warn(unicode(e))
330                 continue
331             outqueue.put(p[-1])
332
333 class RemoveCommand (PlaylistCommand):
334     """Remove a curve from a playlist.
335     """
336     def __init__(self, plugin):
337         super(RemoveCommand, self).__init__(
338             name='remove curve from playlist',
339             arguments=[
340                 Argument(name='index', type='int', optional=False, help="""
341 Index of target curve.
342 """.strip()),
343                 ],
344             help=self.__doc__, plugin=plugin)
345
346     def _run(self, hooke, inqueue, outqueue, params):
347         playlist = self._playlist(hooke, params)
348         playlist.pop(params['index'])
349         playlist.jump(playlist.index())
350
351
352 class ApplyCommand (PlaylistCommand):
353     """Apply a :class:`~hooke.command_stack.CommandStack` to each
354     curve in a playlist.
355
356     TODO: discuss `evaluate`.
357     """
358     def __init__(self, plugin):
359         super(ApplyCommand, self).__init__(
360             name='apply command stack to playlist',
361             arguments=[
362                 Argument(name='commands', type='command stack',
363                          help="""
364 Command stack to apply to each curve.  Defaults to the `command_stack`
365 plugin's current stack.
366 """.strip()),
367                 Argument(name='evaluate', type='bool', default=False,
368                          help="""
369 Evaluate the applied command stack immediately.
370 """.strip()),
371                 ],
372             help=self.__doc__, plugin=plugin)
373
374     def _run(self, hooke, inqueue, outqueue, params):
375         params = self._setup_params(hooke=hooke, params=params)
376         p = self._playlist(hooke, params)
377         if params['evaluate'] == True:
378             exec_cmd = hooke.command_by_name['execute command stack']
379             for curve in p.items():
380                 hooke.run_command(exec_cmd.name,
381                                   {'commands':params['commands'],
382                                    'stack':True})
383         else:
384             for curve in p:
385                 for command in params['commands']:
386                     curve.command_stack.append(command)
387                 curve.set_hooke(hooke)
388                 p.unload(curve)
389
390     def _setup_params(self, hooke, params):
391         if params['commands'] == None:
392             cstack_plugin = [p for p in hooke.plugins
393                              if p.name == 'command_stack'][0]
394             params['commands'] = cstack_plugin.command_stack
395         return params
396
397
398 class FilterCommand (PlaylistAddingCommand, PlaylistCommand):
399     """Create a subset playlist via a selection function.
400
401     Removing lots of curves one at a time can be tedious.  With this
402     command you can use a function `filter` to select the curves you
403     wish to keep.
404
405     Notes
406     -----
407     There are issues with pickling functions bound to class
408     attributes, because the pickle module doesn't know where those
409     functions were originally defined (where it should point the
410     loader).  Because of this, subclasses with hard-coded filter
411     functions are encouraged to define their filter function as a
412     method of their subclass.  See, for example,
413     :meth:`NoteFilterCommand.filter`.
414     """
415     def __init__(self, plugin, name='filter playlist', load_curves=True):
416         super(FilterCommand, self).__init__(
417             name=name, help=self.__doc__, plugin=plugin)
418         self._load_curves = load_curves
419         if not hasattr(self, 'filter'):
420             self.arguments.append(
421                 Argument(name='filter', type='function', optional=False,
422                          help="""
423 Function returning `True` for "good" curves.
424 `filter(curve, hooke, inqueue, outqueue, params) -> True/False`.
425 """.strip()))
426
427     def _run(self, hooke, inqueue, outqueue, params):
428         if not hasattr(self, 'filter'):
429             filter_fn = params['filter']
430         else:
431             filter_fn = self.filter
432         p = self._playlist(hooke, params).filter(
433             filter_fn, load_curves=self._load_curves,
434             hooke=hooke, inqueue=inqueue, outqueue=outqueue, params=params)
435         self._set_playlist(hooke, params, p)
436         if hasattr(p, 'path') and p.path != None:
437             p.set_path(os.path.join(os.path.dirname(p.path), p.name))
438         outqueue.put(p)