Moved system commands from hooke_cli to plugin.system.
[hooke.git] / hooke / playlist.py
1 """The `playlist` module provides a :class:`Playlist` and its subclass
2 :class:`FilePlaylist` for manipulating lists of
3 :class:`hooke.curve.Curve`\s.
4 """
5
6 import copy
7 import hashlib
8 import os.path
9 import xml.dom.minidom
10
11 from . import curve as curve
12
13
14 class NoteIndexList (list):
15     """A list that keeps track of a "current" item and additional notes.
16
17     :attr:`index` (i.e. "bookmark") is the index of the currently
18     current curve.  Also keep a :class:`dict` of additional information
19     (:attr:`info`).
20     """
21     def __init__(self, name=None):
22         super(NoteIndexList, self).__init__()
23         self.name = name
24         self.info = {}
25         self._index = 0
26
27     def current(self):
28         if len(self) == 0:
29             return None
30         return self[self._index]
31
32     def jump(self, index):
33         if len(self) == 0:
34             self._index = 0
35         else:
36             self._index = index % len(self)
37
38     def next(self):
39         self.jump(self._index + 1)
40
41     def previous(self):
42         self.jump(self._index - 1)
43
44     def filter(self, keeper_fn=lambda item:True):
45         c = copy.deepcopy(self)
46         for item in reversed(c):
47             if keeper_fn(item) != True:
48                 c.remove(item)
49         try: # attempt to maintain the same current item
50             c._index = c.index(self.current())
51         except ValueError:
52             c._index = 0
53         return c
54
55 class Playlist (NoteIndexList):
56     """A :class:`NoteIndexList` of :class:`hooke.curve.Curve`\s.
57
58     Keeps a list of :attr:`drivers` for loading curves.
59     """
60     def __init__(self, drivers, name=None):
61         super(Playlist, self).__init__(name=name)
62         self.drivers = drivers
63
64     def append_curve_by_path(self, path, info=None, identify=True):
65         if self.path != None:
66             path = os.path.join(self.path, path)
67         path = os.path.normpath(path)
68         c = curve.Curve(path, info=info)
69         if identify == True:
70             c.identify(self.drivers)
71         self.append(c)
72         return c
73
74 class FilePlaylist (Playlist):
75     version = '0.1'
76
77     def __init__(self, drivers, name=None, path=None):
78         super(FilePlaylist, self).__init__(drivers, name)
79         self.set_path(path)
80         self._digest = None
81
82     def set_path(self, path):
83         if not path.endswith('.hkp'):
84             path += '.hkp'
85         if name == None and path != None:
86             name = os.path.basename(path)
87
88     def is_saved(self):
89         return self.digest() == self._digest
90
91     def digest(self):
92         r"""Compute the sha1 digest of the flattened playlist
93         representation.
94
95         Examples
96         --------
97
98         >>> root_path = os.path.sep + 'path'
99         >>> p = FilePlaylist(drivers=[],
100         ...                  path=os.path.join(root_path, 'to','playlist'))
101         >>> p.info['note'] = 'An example playlist'
102         >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'one'))
103         >>> c.info['note'] = 'The first curve'
104         >>> p.append(c)
105         >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'two'))
106         >>> c.info['note'] = 'The second curve'
107         >>> p.append(c)
108         >>> p.digest()
109         "\xa1\x99\x8a\x99\xed\xad\x13'\xa7w\x12\x00\x07Z\xb3\xd0zN\xa2\xe1"
110         """
111         string = self.flatten()
112         return hashlib.sha1(string).digest()
113
114     def flatten(self, absolute_paths=False):
115         """Create a string representation of the playlist.
116
117         A playlist is an XML document with the following syntax::
118
119             <?xml version="1.0" encoding="utf-8"?>
120             <playlist attribute="value">
121               <curve path="/my/file/path/"/ attribute="value" ...>
122               <curve path="...">
123             </playlist>
124
125         Relative paths are interpreted relative to the location of the
126         playlist file.
127         
128         Examples
129         --------
130
131         >>> root_path = os.path.sep + 'path'
132         >>> p = FilePlaylist(drivers=[],
133         ...                  path=os.path.join(root_path, 'to','playlist'))
134         >>> p.info['note'] = 'An example playlist'
135         >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'one'))
136         >>> c.info['note'] = 'The first curve'
137         >>> p.append(c)
138         >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'two'))
139         >>> c.info['note'] = 'The second curve'
140         >>> p.append(c)
141         >>> print p.flatten() # doctest: +NORMALIZE_WHITESPACE +REPORT_UDIFF
142         <?xml version="1.0" encoding="utf-8"?>
143         <playlist index="0" note="An example playlist" version="0.1">
144             <curve note="The first curve" path="../curve/one"/>
145             <curve note="The second curve" path="../curve/two"/>
146         </playlist>
147         <BLANKLINE>
148         >>> print p.flatten(absolute_paths=True) # doctest: +NORMALIZE_WHITESPACE +REPORT_UDIFF
149         <?xml version="1.0" encoding="utf-8"?>
150         <playlist index="0" note="An example playlist" version="0.1">
151             <curve note="The first curve" path="/path/to/curve/one"/>
152             <curve note="The second curve" path="/path/to/curve/two"/>
153         </playlist>
154         <BLANKLINE>
155         """
156         implementation = xml.dom.minidom.getDOMImplementation()
157         # create the document DOM object and the root element
158         doc = implementation.createDocument(None, 'playlist', None)
159         root = doc.documentElement
160         root.setAttribute('version', self.version) # store playlist version
161         root.setAttribute('index', str(self._index))
162         for key,value in self.info.items(): # save info variables
163             root.setAttribute(key, str(value))
164         for curve in self: # save curves and their attributes
165             curve_element = doc.createElement('curve')
166             root.appendChild(curve_element)
167             path = os.path.abspath(os.path.expanduser(curve.path))
168             if absolute_paths == False:
169                 path = os.path.relpath(
170                     path,
171                     os.path.abspath(os.path.expanduser(self.path)))
172             curve_element.setAttribute('path', path)
173             for key,value in curve.info.items():
174                 curve_element.setAttribute(key, str(value))
175         string = doc.toprettyxml(encoding='utf-8')
176         root.unlink() # break circular references for garbage collection
177         return string
178
179     def _from_xml_doc(self, doc):
180         """Load a playlist from an :class:`xml.dom.minidom.Document`
181         instance.
182         """
183         root = doc.documentElement
184         for attribute,value in root.attributes.items():
185             if attribute == 'version':
186                 assert value == self.version, \
187                     'Cannot read v%s playlist with a v%s reader' \
188                     % (value, self.version)
189             elif attribute == 'index':
190                 self._index = int(value)
191             else:
192                 self.info[attribute] = value
193         for curve_element in doc.getElementsByTagName('curve'):
194             path = curve_element.getAttribute('path')
195             info = dict(curve_element.attributes.items())
196             info.pop('path')
197             self.append_curve_by_path(path, info, identify=False)
198         self.jump(self._index) # ensure valid index
199
200     def from_string(self, string):
201         """Load a playlist from a string.
202
203         Examples
204         --------
205
206         >>> string = '''<?xml version="1.0" encoding="utf-8"?>
207         ... <playlist index="1" note="An example playlist" version="0.1">
208         ...     <curve note="The first curve" path="../curve/one"/>
209         ...     <curve note="The second curve" path="../curve/two"/>
210         ... </playlist>
211         ... '''
212         >>> p = FilePlaylist(drivers=[],
213         ...                  path=os.path.join('path', 'to','playlist'))
214         >>> p.from_string(string)
215         >>> p._index
216         1
217         >>> p.info
218         {u'note': u'An example playlist'}
219         >>> for curve in p:
220         ...     print curve.path
221         path/to/curve/one
222         path/to/curve/two
223         """
224         doc = xml.dom.minidom.parseString(string)
225         return self._from_xml_doc(doc)
226
227     def load(self, path=None):
228         """Load a playlist from a file.
229         """
230         self.set_path(path)
231         doc = xml.dom.minidom.parse(self.path)
232         return self._from_xml_doc(doc)
233
234     def save(self, path=None):
235         """Saves the playlist in a XML file.
236         """
237         self.set_path(path)
238         f = file(self.path, 'w')
239         f.write(self.flatten())
240         f.close()