58832d3537e39891b28a9c8981e62fc6aada887d
[hooke.git] / hooke / plugin / playlist.py
1 """Defines :class:`PlaylistPlugin` several associated
2 :class:`hooke.plugin.Command`\s.
3 """
4
5 import copy
6 import hashlib
7 import os
8 import os.path
9 import xml.dom.minidom
10
11
12 from .. import curve as curve
13 from ..plugin import Plugin, Command, Argument
14
15 class PlaylistPlugin (Plugin):
16     def __init__(self):
17         super(PlaylistPlugin, self).__init__(name='playlist')
18
19     def commands(self):
20         return [NextCommand(), PreviousCommand(), JumpCommand(),
21                 SaveCommand(), LoadCommand(),
22                 AddCommand(), RemoveCommand(), FilterCommand()]
23
24 class Playlist (list):
25     """A list of :class:`hooke.curve.Curve`\s.
26
27     Keeps a list of :attr:`drivers` for loading curves, the
28     :attr:`index` (i.e. "bookmark") of the currently active curve, and
29     a :class:`dict` of additional informtion (:attr:`info`).
30     """
31     def __init__(self, drivers, name=None):
32         super(Playlist, self).__init__()
33         self.drivers = drivers
34         self.name = name
35         self.info = {}
36         self._index = 0
37
38     def append_curve_by_path(self, path, info=None, identify=True):
39         if self.path != None:
40             path = os.path.join(self.path, path)
41         path = os.path.normpath(path)
42         c = curve.Curve(path, info=info)
43         if identify == True:
44             c.identify(self.drivers)
45         self.append(c)
46         return c
47
48     def active_curve(self):
49         return self[self._index]
50
51     def has_curves(self):
52         return len(self) > 0
53
54     def jump(self, index):
55         if len(self) == 0:
56             self._index = 0
57         else:
58             self._index = index % len(self)
59
60     def next(self):
61         self.jump(self._index + 1)
62
63     def previous(self):
64         self.jump(self._index - 1)
65
66     def filter(self, keeper_fn=lambda curve:True):
67         playlist = copy.deepcopy(self)
68         for curve in reversed(playlist.curves):
69             if keeper_fn(curve) != True:
70                 playlist.curves.remove(curve)
71         try: # attempt to maintain the same active curve
72             playlist._index = playlist.index(self.active_curve())
73         except ValueError:
74             playlist._index = 0
75         return playlist
76
77 class FilePlaylist (Playlist):
78     version = '0.1'
79
80     def __init__(self, drivers, name=None, path=None):
81         if name == None and path != None:
82             name = os.path.basename(path)
83         super(FilePlaylist, self).__init__(drivers, name)
84         self.path = path
85         self._digest = None
86
87     def is_saved(self):
88         return self.digest() == self._digest
89
90     def digest(self):
91         r"""Compute the sha1 digest of the flattened playlist
92         representation.
93
94         Examples
95         --------
96
97         >>> root_path = os.path.sep + 'path'
98         >>> p = FilePlaylist(drivers=[],
99         ...                  path=os.path.join(root_path, 'to','playlist'))
100         >>> p.info['note'] = 'An example playlist'
101         >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'one'))
102         >>> c.info['note'] = 'The first curve'
103         >>> p.append(c)
104         >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'two'))
105         >>> c.info['note'] = 'The second curve'
106         >>> p.append(c)
107         >>> p.digest()
108         "\xa1\x99\x8a\x99\xed\xad\x13'\xa7w\x12\x00\x07Z\xb3\xd0zN\xa2\xe1"
109         """
110         string = self.flatten()
111         return hashlib.sha1(string).digest()
112
113     def flatten(self, absolute_paths=False):
114         """Create a string representation of the playlist.
115
116         A playlist is an XML document with the following syntax::
117
118             <?xml version="1.0" encoding="utf-8"?>
119             <playlist attribute="value">
120               <curve path="/my/file/path/"/ attribute="value" ...>
121               <curve path="...">
122             </playlist>
123
124         Relative paths are interpreted relative to the location of the
125         playlist file.
126         
127         Examples
128         --------
129
130         >>> root_path = os.path.sep + 'path'
131         >>> p = FilePlaylist(drivers=[],
132         ...                  path=os.path.join(root_path, 'to','playlist'))
133         >>> p.info['note'] = 'An example playlist'
134         >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'one'))
135         >>> c.info['note'] = 'The first curve'
136         >>> p.append(c)
137         >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'two'))
138         >>> c.info['note'] = 'The second curve'
139         >>> p.append(c)
140         >>> print p.flatten() # doctest: +NORMALIZE_WHITESPACE +REPORT_UDIFF
141         <?xml version="1.0" encoding="utf-8"?>
142         <playlist index="0" note="An example playlist" version="0.1">
143             <curve note="The first curve" path="../curve/one"/>
144             <curve note="The second curve" path="../curve/two"/>
145         </playlist>
146         <BLANKLINE>
147         >>> print p.flatten(absolute_paths=True) # doctest: +NORMALIZE_WHITESPACE +REPORT_UDIFF
148         <?xml version="1.0" encoding="utf-8"?>
149         <playlist index="0" note="An example playlist" version="0.1">
150             <curve note="The first curve" path="/path/to/curve/one"/>
151             <curve note="The second curve" path="/path/to/curve/two"/>
152         </playlist>
153         <BLANKLINE>
154         """
155         implementation = xml.dom.minidom.getDOMImplementation()
156         # create the document DOM object and the root element
157         doc = implementation.createDocument(None, 'playlist', None)
158         root = doc.documentElement
159         root.setAttribute('version', self.version) # store playlist version
160         root.setAttribute('index', str(self._index))
161         for key,value in self.info.items(): # save info variables
162             root.setAttribute(key, str(value))
163         for curve in self: # save curves and their attributes
164             curve_element = doc.createElement('curve')
165             root.appendChild(curve_element)
166             path = os.path.abspath(os.path.expanduser(curve.path))
167             if absolute_paths == False:
168                 path = os.path.relpath(
169                     path,
170                     os.path.abspath(os.path.expanduser(self.path)))
171             curve_element.setAttribute('path', path)
172             for key,value in curve.info.items():
173                 curve_element.setAttribute(key, str(value))
174         string = doc.toprettyxml(encoding='utf-8')
175         root.unlink() # break circular references for garbage collection
176         return string
177
178     def _from_xml_doc(self, doc):
179         """Load a playlist from an :class:`xml.dom.minidom.Document`
180         instance.
181         """
182         root = doc.documentElement
183         for attribute,value in root.attributes.items():
184             if attribute == 'version':
185                 assert value == self.version, \
186                     'Cannot read v%s playlist with a v%s reader' \
187                     % (value, self.version)
188             elif attribute == 'index':
189                 self._index = int(value)
190             else:
191                 self.info[attribute] = value
192         for curve_element in doc.getElementsByTagName('curve'):
193             path = curve_element.getAttribute('path')
194             info = dict(curve_element.attributes.items())
195             info.pop('path')
196             self.append_curve_by_path(path, info, identify=False)
197         self.jump(self._index) # ensure valid index
198
199     def from_string(self, string):
200         """Load a playlist from a string.
201
202         Examples
203         --------
204
205         >>> string = '''<?xml version="1.0" encoding="utf-8"?>
206         ... <playlist index="1" note="An example playlist" version="0.1">
207         ...     <curve note="The first curve" path="../curve/one"/>
208         ...     <curve note="The second curve" path="../curve/two"/>
209         ... </playlist>
210         ... '''
211         >>> p = FilePlaylist(drivers=[],
212         ...                  path=os.path.join('path', 'to','playlist'))
213         >>> p.from_string(string)
214         >>> p._index
215         1
216         >>> p.info
217         {u'note': u'An example playlist'}
218         >>> for curve in p:
219         ...     print curve.path
220         path/to/curve/one
221         path/to/curve/two
222         """
223         doc = xml.dom.minidom.parseString(string)
224         return self._from_xml_doc(doc)
225
226     def load(self, path=None):
227         """Load a playlist from a file.
228         """
229         if path != None:
230             self.path = path
231         if self.name == None:
232             self.name = os.path.basename(self.path)
233         doc = xml.dom.minidom.parse(path)
234         return self._from_xml_doc(doc)
235
236     def save(self, path):
237         """Saves the playlist in a XML file.
238         """
239         f = file(path, 'w')
240         f.write(self.flatten())
241         f.close()
242
243 class NextCommand (Command):
244     """Move playlist to the next curve.
245     """
246     def __init__(self):
247         super(NextCommand, self).__init__(
248             name='next curve',
249             arguments=[
250                 Argument(name='playlist', type='playlist', optional=False,
251                          help="""
252 :class:``hooke.plugin.playlist.Playlist`` to act on.
253 """.strip()),
254                 ],
255             help=self.__doc__)
256
257     def _run(inqueue, outqueue, params):
258         params['playlist'].next()
259
260 class PreviousCommand (Command):
261     """Move playlist to the previous curve.
262     """
263     def __init__(self):
264         super(PreviousCommand, self).__init__(
265             name='previous curve',
266             arguments=[
267                 Argument(name='playlist', type='playlist', optional=False,
268                          help="""
269 :class:``hooke.plugin.playlist.Playlist`` to act on.
270 """.strip()),
271                 ],
272             help=self.__doc__)
273
274     def _run(inqueue, outqueue, params):
275         params['playlist'].previous()
276
277 class JumpCommand (Command):
278     """Move playlist to a given curve.
279     """
280     def __init__(self):
281         super(JumpCommand, self).__init__(
282             name='jump to curve',
283             arguments=[
284                 Argument(name='playlist', type='playlist', optional=False,
285                          help="""
286 :class:``hooke.plugin.playlist.Playlist`` to act on.
287 """.strip()),
288                 Argument(name='index', type='int', optional=False, help="""
289 Index of target curve.
290 """.strip()),
291                 ],
292             help=self.__doc__)
293
294     def _run(inqueue, outqueue, params):
295         params['playlist'].jump(params['index'])
296
297 class SaveCommand (Command):
298     """Save a playlist.
299     """
300     def __init__(self):
301         super(SaveCommand, self).__init__(
302             name='save playlist',
303             arguments=[
304                 Argument(name='playlist', type='playlist', optional=False,
305                          help="""
306 :class:``hooke.plugin.playlist.Playlist`` to act on.
307 """.strip()),
308                 Argument(name='output', type='file',
309                          help="""
310 File name for the output playlist.  Defaults to overwring the input
311 playlist.
312 """.strip()),
313                 ],
314             help=self.__doc__)
315
316     def _run(inqueue, outqueue, params):
317         params['playlist'].save(params['output'])
318
319 class LoadCommand (Command):
320     """Load a playlist.
321     """
322     def __init__(self):
323         super(LoadCommand, self).__init__(
324             name='load playlist',
325             arguments=[
326                 Argument(name='input', type='file', optional=False,
327                          help="""
328 File name for the input playlist.
329 """.strip()),
330                 Argument(name='digests', type='digest', optional=False,
331                          count=-1,
332                          help="""
333 Digests for loading curves.
334 """.strip()),
335                 ],
336             help=self.__doc__)
337
338     def _run(inqueue, outqueue, params):
339         p = FilePlaylist(drivers=params['drivers'], path=params['input'])
340         p.load()
341         outqueue.put(p)
342
343 class AddCommand (Command):
344     """Add a curve to a playlist.
345     """
346     def __init__(self):
347         super(AddCommand, self).__init__(
348             name='add curve to playlist',
349             arguments=[
350                 Argument(name='playlist', type='playlist', optional=False,
351                          help="""
352 :class:``hooke.plugin.playlist.Playlist`` to act on.
353 """.strip()),
354                 Argument(name='input', type='file', optional=False,
355                          help="""
356 File name for the input :class:`hooke.curve.Curve`.
357 """.strip()),
358                 Argument(name='info', type='dict', optional=True,
359                          help="""
360 Additional information for the input :class:`hooke.curve.Curve`.
361 """.strip()),
362                 ],
363             help=self.__doc__)
364
365     def _run(inqueue, outqueue, params):
366         params['playlist'].append_curve_by_path(params['input'],
367                                                 params['info'])
368
369 class RemoveCommand (Command):
370     """Remove a curve from a playlist.
371     """
372     def __init__(self):
373         super(RemoveCommand, self).__init__(
374             name='remove curve from playlist',
375             arguments=[
376                 Argument(name='playlist', type='playlist', optional=False,
377                          help="""
378 :class:``hooke.plugin.playlist.Playlist`` to act on.
379 """.strip()),
380                 Argument(name='index', type='int', optional=False, help="""
381 Index of target curve.
382 """.strip()),
383                 ],
384             help=self.__doc__)
385
386     def _run(inqueue, outqueue, params):
387         params['playlist'].pop(params['index'])
388         params['playlist'].jump(params._index)
389
390 class FilterCommand (Command):
391     """Create a subset playlist via a selection function.
392     """
393     def __init__(self):
394         super(FilterCommand, self).__init__(
395             name='filter playlist',
396             arguments=[
397                 Argument(name='playlist', type='playlist', optional=False,
398                          help="""
399 :class:``hooke.plugin.playlist.Playlist`` to act on.
400 """.strip()),
401                 Argument(name='filter', type='function', optional=False,
402                          help="""
403 Function returning `True` for "good" curves.
404 """.strip()),
405                 ],
406             help=self.__doc__)
407
408     def _run(inqueue, outqueue, params):
409         p = params['playlist'].filter(params['filter'])
410         outqueue.put(p)