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