1 # Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
3 # This file is part of Hooke.
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.
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.
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/>.
19 """The `playlist` module provides a :class:`Playlist` and its subclass
20 :class:`FilePlaylist` for manipulating lists of
21 :class:`hooke.curve.Curve`\s.
27 import xml.dom.minidom
29 from . import curve as curve
30 from .compat import minidom as minidom # dynamically patch xml.sax.minidom
33 class NoteIndexList (list):
34 """A list that keeps track of a "current" item and additional notes.
36 :attr:`index` (i.e. "bookmark") is the index of the currently
37 current curve. Also keep a :class:`dict` of additional information
40 def __init__(self, name=None):
41 super(NoteIndexList, self).__init__()
47 return '<%s %s>' % (self.__class__.__name__, self.name)
49 def _setup_item(self, item):
50 """Perform any required initialization before returning an item.
57 item = self[self._index]
58 self._setup_item(item)
61 def jump(self, index):
65 self._index = index % len(self)
68 self.jump(self._index + 1)
71 self.jump(self._index - 1)
73 def filter(self, keeper_fn=lambda item:True, *args, **kwargs):
74 c = copy.deepcopy(self)
75 for item in reversed(c):
77 if keeper_fn(item, *args, **kwargs) != True:
79 try: # attempt to maintain the same current item
80 c._index = c.index(self.current())
85 class Playlist (NoteIndexList):
86 """A :class:`NoteIndexList` of :class:`hooke.curve.Curve`\s.
88 Keeps a list of :attr:`drivers` for loading curves.
90 def __init__(self, drivers, name=None):
91 super(Playlist, self).__init__(name=name)
92 self.drivers = drivers
93 self._loaded = [] # List of loaded curves, see :meth:`._setup_item`.
94 self._max_loaded = 100 # curves to hold in memory simultaneously.
96 def append_curve_by_path(self, path, info=None, identify=True):
98 path = os.path.join(os.path.dirname(self.path), path)
99 path = os.path.normpath(path)
100 c = curve.Curve(path, info=info)
102 c.identify(self.drivers)
106 def _setup_item(self, curve):
107 if curve != None and curve not in self._loaded:
108 if curve not in self:
110 if curve.driver == None:
111 c.identify(self.drivers)
112 if curve.data == None:
114 self._loaded.append(curve)
115 if len(self._loaded) > self._max_loaded:
116 oldest = self._loaded.pop(0)
119 class FilePlaylist (Playlist):
122 def __init__(self, drivers, name=None, path=None):
123 super(FilePlaylist, self).__init__(drivers, name)
126 self._ignored_keys = [
127 'experiment', # class instance, not very exciting.
130 def set_path(self, path):
132 if not path.endswith('.hkp'):
135 if self.name == None:
136 self.name = os.path.basename(path)
139 return self.digest() == self._digest
142 r"""Compute the sha1 digest of the flattened playlist
148 >>> root_path = os.path.sep + 'path'
149 >>> p = FilePlaylist(drivers=[],
150 ... path=os.path.join(root_path, 'to','playlist'))
151 >>> p.info['note'] = 'An example playlist'
152 >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'one'))
153 >>> c.info['note'] = 'The first curve'
155 >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'two'))
156 >>> c.info['note'] = 'The second curve'
159 '\\\x14\x87\x88*q\xf8\xaa\xa7\x84f\x82\xa1S>\xfd3+\xd0o'
161 string = self.flatten()
162 return hashlib.sha1(string).digest()
164 def _clean_key(self, key):
165 """Replace spaces in keys with \\u00B7 (middle dot).
167 This character is deemed unlikely to occur in keys to our
168 playlist and curve info dictionaries, while many keys have
171 \\u00B7 is allowed in XML 1.0 as of the 5th edition. See
172 the `4th edition errata`_ for details.
174 .. _4th edition errata:
175 http://www.w3.org/XML/xml-V10-4e-errata#E09
177 return key.replace(' ', u'\u00B7')
179 def _restore_key(self, key):
180 """Restore keys encoded with :meth:`_clean_key`.
182 return key.replace(u'\u00B7', ' ')
184 def flatten(self, absolute_paths=False):
185 """Create a string representation of the playlist.
187 A playlist is an XML document with the following syntax::
189 <?xml version="1.0" encoding="utf-8"?>
190 <playlist attribute="value">
191 <curve path="/my/file/path/"/ attribute="value" ...>
195 Relative paths are interpreted relative to the location of the
201 >>> root_path = os.path.sep + 'path'
202 >>> p = FilePlaylist(drivers=[],
203 ... path=os.path.join(root_path, 'to','playlist'))
204 >>> p.info['note'] = 'An example playlist'
205 >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'one'))
206 >>> c.info['note'] = 'The first curve'
208 >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'two'))
209 >>> c.info['attr with spaces'] = 'The second curve\\nwith endlines'
211 >>> def _print(string):
212 ... escaped_string = unicode(string, 'utf-8').encode('unicode escape')
213 ... print escaped_string.replace('\\\\n', '\\n').replace('\\\\t', '\\t'),
214 >>> _print(p.flatten()) # doctest: +NORMALIZE_WHITESPACE +REPORT_UDIFF
215 <?xml version="1.0" encoding="utf-8"?>
216 <playlist index="0" note="An example playlist" version="0.1">
217 <curve note="The first curve" path="curve/one"/>
218 <curve attr\\xb7with\\xb7spaces="The second curve
with endlines" path="curve/two"/>
220 >>> _print(p.flatten(absolute_paths=True)) # doctest: +NORMALIZE_WHITESPACE +REPORT_UDIFF
221 <?xml version="1.0" encoding="utf-8"?>
222 <playlist index="0" note="An example playlist" version="0.1">
223 <curve note="The first curve" path="/path/to/curve/one"/>
224 <curve attr\\xb7with\\xb7spaces="The second curve
with endlines" path="/path/to/curve/two"/>
227 implementation = xml.dom.minidom.getDOMImplementation()
228 # create the document DOM object and the root element
229 doc = implementation.createDocument(None, 'playlist', None)
230 root = doc.documentElement
231 root.setAttribute('version', self.version) # store playlist version
232 root.setAttribute('index', str(self._index))
233 for key,value in self.info.items(): # save info variables
234 root.setAttribute(self._clean_key(key), str(value))
235 for curve in self: # save curves and their attributes
236 curve_element = doc.createElement('curve')
237 root.appendChild(curve_element)
238 path = os.path.abspath(os.path.expanduser(curve.path))
239 if absolute_paths == False:
240 path = os.path.relpath(
244 os.path.expanduser(self.path))))
245 curve_element.setAttribute('path', path)
246 for key,value in curve.info.items():
247 if key in self._ignored_keys:
249 curve_element.setAttribute(self._clean_key(key), str(value))
250 string = doc.toprettyxml(encoding='utf-8')
251 root.unlink() # break circular references for garbage collection
254 def _from_xml_doc(self, doc, identify=True):
255 """Load a playlist from an :class:`xml.dom.minidom.Document`
258 root = doc.documentElement
259 for attribute,value in root.attributes.items():
260 attribute = self._restore_key(attribute)
261 if attribute == 'version':
262 assert value == self.version, \
263 'Cannot read v%s playlist with a v%s reader' \
264 % (value, self.version)
265 elif attribute == 'index':
266 self._index = int(value)
268 self.info[attribute] = value
269 for curve_element in doc.getElementsByTagName('curve'):
270 path = curve_element.getAttribute('path')
271 info = dict([(self._restore_key(key), value)
272 for key,value in curve_element.attributes.items()])
274 self.append_curve_by_path(path, info, identify=identify)
275 self.jump(self._index) # ensure valid index
277 def from_string(self, string, identify=True):
278 u"""Load a playlist from a string.
283 >>> string = '''<?xml version="1.0" encoding="utf-8"?>
284 ... <playlist index="1" note="An example playlist" version="0.1">
285 ... <curve note="The first curve" path="../curve/one"/>
286 ... <curve attr\xb7with\xb7spaces="The second curve
with endlines" path="../curve/two"/>
289 >>> p = FilePlaylist(drivers=[],
290 ... path=os.path.join('path', 'to', 'my', 'playlist'))
291 >>> p.from_string(string, identify=False)
295 {u'note': u'An example playlist'}
300 >>> p[-1].info['attr with spaces']
301 u'The second curve\\nwith endlines'
303 doc = xml.dom.minidom.parseString(string)
304 self._from_xml_doc(doc, identify=identify)
306 def load(self, path=None, identify=True):
307 """Load a playlist from a file.
310 doc = xml.dom.minidom.parse(self.path)
311 self._from_xml_doc(doc, identify=identify)
312 self._digest = self.digest()
314 def save(self, path=None):
315 """Saves the playlist in a XML file.
318 f = file(self.path, 'w')
319 f.write(self.flatten())