Fix typos in hooke.playlist.FilePlaylist.set_path
[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 path != None:
84             if not path.endswith('.hkp'):
85                 path += '.hkp'
86             if self.name == None:
87                 name = os.path.basename(path)
88
89     def is_saved(self):
90         return self.digest() == self._digest
91
92     def digest(self):
93         r"""Compute the sha1 digest of the flattened playlist
94         representation.
95
96         Examples
97         --------
98
99         >>> root_path = os.path.sep + 'path'
100         >>> p = FilePlaylist(drivers=[],
101         ...                  path=os.path.join(root_path, 'to','playlist'))
102         >>> p.info['note'] = 'An example playlist'
103         >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'one'))
104         >>> c.info['note'] = 'The first curve'
105         >>> p.append(c)
106         >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'two'))
107         >>> c.info['note'] = 'The second curve'
108         >>> p.append(c)
109         >>> p.digest()
110         "\xa1\x99\x8a\x99\xed\xad\x13'\xa7w\x12\x00\x07Z\xb3\xd0zN\xa2\xe1"
111         """
112         string = self.flatten()
113         return hashlib.sha1(string).digest()
114
115     def flatten(self, absolute_paths=False):
116         """Create a string representation of the playlist.
117
118         A playlist is an XML document with the following syntax::
119
120             <?xml version="1.0" encoding="utf-8"?>
121             <playlist attribute="value">
122               <curve path="/my/file/path/"/ attribute="value" ...>
123               <curve path="...">
124             </playlist>
125
126         Relative paths are interpreted relative to the location of the
127         playlist file.
128         
129         Examples
130         --------
131
132         >>> root_path = os.path.sep + 'path'
133         >>> p = FilePlaylist(drivers=[],
134         ...                  path=os.path.join(root_path, 'to','playlist'))
135         >>> p.info['note'] = 'An example playlist'
136         >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'one'))
137         >>> c.info['note'] = 'The first curve'
138         >>> p.append(c)
139         >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'two'))
140         >>> c.info['note'] = 'The second curve'
141         >>> p.append(c)
142         >>> print p.flatten() # doctest: +NORMALIZE_WHITESPACE +REPORT_UDIFF
143         <?xml version="1.0" encoding="utf-8"?>
144         <playlist index="0" note="An example playlist" version="0.1">
145             <curve note="The first curve" path="../curve/one"/>
146             <curve note="The second curve" path="../curve/two"/>
147         </playlist>
148         <BLANKLINE>
149         >>> print p.flatten(absolute_paths=True) # doctest: +NORMALIZE_WHITESPACE +REPORT_UDIFF
150         <?xml version="1.0" encoding="utf-8"?>
151         <playlist index="0" note="An example playlist" version="0.1">
152             <curve note="The first curve" path="/path/to/curve/one"/>
153             <curve note="The second curve" path="/path/to/curve/two"/>
154         </playlist>
155         <BLANKLINE>
156         """
157         implementation = xml.dom.minidom.getDOMImplementation()
158         # create the document DOM object and the root element
159         doc = implementation.createDocument(None, 'playlist', None)
160         root = doc.documentElement
161         root.setAttribute('version', self.version) # store playlist version
162         root.setAttribute('index', str(self._index))
163         for key,value in self.info.items(): # save info variables
164             root.setAttribute(key, str(value))
165         for curve in self: # save curves and their attributes
166             curve_element = doc.createElement('curve')
167             root.appendChild(curve_element)
168             path = os.path.abspath(os.path.expanduser(curve.path))
169             if absolute_paths == False:
170                 path = os.path.relpath(
171                     path,
172                     os.path.abspath(os.path.expanduser(self.path)))
173             curve_element.setAttribute('path', path)
174             for key,value in curve.info.items():
175                 curve_element.setAttribute(key, str(value))
176         string = doc.toprettyxml(encoding='utf-8')
177         root.unlink() # break circular references for garbage collection
178         return string
179
180     def _from_xml_doc(self, doc):
181         """Load a playlist from an :class:`xml.dom.minidom.Document`
182         instance.
183         """
184         root = doc.documentElement
185         for attribute,value in root.attributes.items():
186             if attribute == 'version':
187                 assert value == self.version, \
188                     'Cannot read v%s playlist with a v%s reader' \
189                     % (value, self.version)
190             elif attribute == 'index':
191                 self._index = int(value)
192             else:
193                 self.info[attribute] = value
194         for curve_element in doc.getElementsByTagName('curve'):
195             path = curve_element.getAttribute('path')
196             info = dict(curve_element.attributes.items())
197             info.pop('path')
198             self.append_curve_by_path(path, info, identify=False)
199         self.jump(self._index) # ensure valid index
200
201     def from_string(self, string):
202         """Load a playlist from a string.
203
204         Examples
205         --------
206
207         >>> string = '''<?xml version="1.0" encoding="utf-8"?>
208         ... <playlist index="1" note="An example playlist" version="0.1">
209         ...     <curve note="The first curve" path="../curve/one"/>
210         ...     <curve note="The second curve" path="../curve/two"/>
211         ... </playlist>
212         ... '''
213         >>> p = FilePlaylist(drivers=[],
214         ...                  path=os.path.join('path', 'to','playlist'))
215         >>> p.from_string(string)
216         >>> p._index
217         1
218         >>> p.info
219         {u'note': u'An example playlist'}
220         >>> for curve in p:
221         ...     print curve.path
222         path/to/curve/one
223         path/to/curve/two
224         """
225         doc = xml.dom.minidom.parseString(string)
226         return self._from_xml_doc(doc)
227
228     def load(self, path=None):
229         """Load a playlist from a file.
230         """
231         self.set_path(path)
232         doc = xml.dom.minidom.parse(self.path)
233         return self._from_xml_doc(doc)
234
235     def save(self, path=None):
236         """Saves the playlist in a XML file.
237         """
238         self.set_path(path)
239         f = file(self.path, 'w')
240         f.write(self.flatten())
241         f.close()