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