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