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.
28 import xml.dom.minidom
30 from . import curve as curve
31 from .compat import minidom as minidom # dynamically patch xml.sax.minidom
34 class NoteIndexList (list):
35 """A list that keeps track of a "current" item and additional notes.
37 :attr:`index` (i.e. "bookmark") is the index of the currently
38 current curve. Also keep a :class:`dict` of additional information
41 def __init__(self, name=None):
42 super(NoteIndexList, self).__init__()
48 return str(self.__unicode__())
50 def __unicode__(self):
51 return u'<%s %s>' % (self.__class__.__name__, self.name)
56 def _setup_item(self, item):
57 """Perform any required initialization before returning an item.
61 def index(self, value=None, *args, **kwargs):
62 """Extend `list.index`, returning the current index if `value`
67 return super(NoteIndexList, self).index(value, *args, **kwargs)
72 item = self[self._index]
73 self._setup_item(item)
76 def jump(self, index):
80 self._index = index % len(self)
83 self.jump(self._index + 1)
86 self.jump(self._index - 1)
88 def items(self, reverse=False):
89 """Iterate through `self` calling `_setup_item` on each item
94 items = reversed(self)
96 self._setup_item(item)
99 def filter(self, keeper_fn=lambda item:True, *args, **kwargs):
100 c = copy.deepcopy(self)
101 for item in c.items(reverse=True):
102 if keeper_fn(item, *args, **kwargs) != True:
104 try: # attempt to maintain the same current item
105 c._index = c.index(self.current())
111 class Playlist (NoteIndexList):
112 """A :class:`NoteIndexList` of :class:`hooke.curve.Curve`\s.
114 Keeps a list of :attr:`drivers` for loading curves.
116 def __init__(self, drivers, name=None):
117 super(Playlist, self).__init__(name=name)
118 self.drivers = drivers
119 self._loaded = [] # List of loaded curves, see :meth:`._setup_item`.
120 self._max_loaded = 100 # curves to hold in memory simultaneously.
122 def append_curve_by_path(self, path, info=None, identify=True):
123 path = os.path.normpath(path)
124 c = curve.Curve(path, info=info)
126 c.identify(self.drivers)
130 def _setup_item(self, curve):
131 if curve != None and curve not in self._loaded:
132 if curve not in self:
134 if curve.driver == None:
135 c.identify(self.drivers)
136 if curve.data == None:
138 self._loaded.append(curve)
139 if len(self._loaded) > self._max_loaded:
140 oldest = self._loaded.pop(0)
144 class FilePlaylist (Playlist):
147 def __init__(self, drivers, name=None, path=None):
148 super(FilePlaylist, self).__init__(drivers, name)
152 self._ignored_keys = [
153 'experiment', # class instance, not very exciting.
156 def set_path(self, path):
158 if not path.endswith('.hkp'):
161 if self.name == None:
162 self.name = os.path.basename(path)
164 def append_curve_by_path(self, path, *args, **kwargs):
165 if self.path != None:
166 path = os.path.join(os.path.dirname(self.path), path)
167 super(FilePlaylist, self).append_curve_by_path(path, *args, **kwargs)
170 return self.digest() == self._digest
173 r"""Compute the sha1 digest of the flattened playlist
179 >>> root_path = os.path.sep + 'path'
180 >>> p = FilePlaylist(drivers=[],
181 ... path=os.path.join(root_path, 'to','playlist'))
182 >>> p.info['note'] = 'An example playlist'
183 >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'one'))
184 >>> c.info['note'] = 'The first curve'
186 >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'two'))
187 >>> c.info['note'] = 'The second curve'
190 '\\\x14\x87\x88*q\xf8\xaa\xa7\x84f\x82\xa1S>\xfd3+\xd0o'
192 string = self.flatten()
193 return hashlib.sha1(string).digest()
195 def _clean_key(self, key):
196 """Replace spaces in keys with \\u00B7 (middle dot).
198 This character is deemed unlikely to occur in keys to our
199 playlist and curve info dictionaries, while many keys have
202 \\u00B7 is allowed in XML 1.0 as of the 5th edition. See
203 the `4th edition errata`_ for details.
205 .. _4th edition errata:
206 http://www.w3.org/XML/xml-V10-4e-errata#E09
208 return key.replace(' ', u'\u00B7')
210 def _restore_key(self, key):
211 """Restore keys encoded with :meth:`_clean_key`.
213 return key.replace(u'\u00B7', ' ')
215 def flatten(self, absolute_paths=False):
216 """Create a string representation of the playlist.
218 A playlist is an XML document with the following syntax::
220 <?xml version="1.0" encoding="utf-8"?>
221 <playlist attribute="value">
222 <curve path="/my/file/path/"/ attribute="value" ...>
226 Relative paths are interpreted relative to the location of the
232 >>> root_path = os.path.sep + 'path'
233 >>> p = FilePlaylist(drivers=[],
234 ... path=os.path.join(root_path, 'to','playlist'))
235 >>> p.info['note'] = 'An example playlist'
236 >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'one'))
237 >>> c.info['note'] = 'The first curve'
239 >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'two'))
240 >>> c.info['attr with spaces'] = 'The second curve\\nwith endlines'
242 >>> def _print(string):
243 ... escaped_string = unicode(string, 'utf-8').encode('unicode escape')
244 ... print escaped_string.replace('\\\\n', '\\n').replace('\\\\t', '\\t'),
245 >>> _print(p.flatten()) # doctest: +NORMALIZE_WHITESPACE +REPORT_UDIFF
246 <?xml version="1.0" encoding="utf-8"?>
247 <playlist index="0" note="An example playlist" version="0.1">
248 <curve note="The first curve" path="curve/one"/>
249 <curve attr\\xb7with\\xb7spaces="The second curve
with endlines" path="curve/two"/>
251 >>> _print(p.flatten(absolute_paths=True)) # doctest: +NORMALIZE_WHITESPACE +REPORT_UDIFF
252 <?xml version="1.0" encoding="utf-8"?>
253 <playlist index="0" note="An example playlist" version="0.1">
254 <curve note="The first curve" path="/path/to/curve/one"/>
255 <curve attr\\xb7with\\xb7spaces="The second curve
with endlines" path="/path/to/curve/two"/>
258 implementation = xml.dom.minidom.getDOMImplementation()
259 # create the document DOM object and the root element
260 doc = implementation.createDocument(None, 'playlist', None)
261 root = doc.documentElement
262 root.setAttribute('version', self.version) # store playlist version
263 root.setAttribute('index', str(self._index))
264 for key,value in self.info.items(): # save info variables
265 if (key in self._ignored_keys
266 or not isinstance(value, types.StringTypes)):
268 root.setAttribute(self._clean_key(key), str(value))
269 for curve in self: # save curves and their attributes
270 curve_element = doc.createElement('curve')
271 root.appendChild(curve_element)
272 path = os.path.abspath(os.path.expanduser(curve.path))
273 if absolute_paths == False:
274 path = os.path.relpath(
278 os.path.expanduser(self.path))))
279 curve_element.setAttribute('path', path)
280 for key,value in curve.info.items():
281 if (key in self._ignored_keys
282 or not isinstance(value, types.StringTypes)):
284 curve_element.setAttribute(self._clean_key(key), str(value))
285 string = doc.toprettyxml(encoding='utf-8')
286 root.unlink() # break circular references for garbage collection
289 def _from_xml_doc(self, doc, identify=True):
290 """Load a playlist from an :class:`xml.dom.minidom.Document`
293 root = doc.documentElement
294 for attribute,value in root.attributes.items():
295 attribute = self._restore_key(attribute)
296 if attribute == 'version':
297 assert value == self.version, \
298 'Cannot read v%s playlist with a v%s reader' \
299 % (value, self.version)
300 elif attribute == 'index':
301 self._index = int(value)
303 self.info[attribute] = value
304 for curve_element in doc.getElementsByTagName('curve'):
305 path = curve_element.getAttribute('path')
306 info = dict([(self._restore_key(key), value)
307 for key,value in curve_element.attributes.items()])
309 self.append_curve_by_path(path, info, identify=identify)
310 self.jump(self._index) # ensure valid index
312 def from_string(self, string, identify=True):
313 u"""Load a playlist from a string.
318 >>> string = '''<?xml version="1.0" encoding="utf-8"?>
319 ... <playlist index="1" note="An example playlist" version="0.1">
320 ... <curve note="The first curve" path="../curve/one"/>
321 ... <curve attr\xb7with\xb7spaces="The second curve
with endlines" path="../curve/two"/>
324 >>> p = FilePlaylist(drivers=[],
325 ... path=os.path.join('path', 'to', 'my', 'playlist'))
326 >>> p.from_string(string, identify=False)
330 {u'note': u'An example playlist'}
335 >>> p[-1].info['attr with spaces']
336 u'The second curve\\nwith endlines'
338 doc = xml.dom.minidom.parseString(string)
339 self._from_xml_doc(doc, identify=identify)
341 def load(self, path=None, identify=True):
342 """Load a playlist from a file.
345 doc = xml.dom.minidom.parse(self.path)
346 self._from_xml_doc(doc, identify=identify)
347 self._digest = self.digest()
349 def save(self, path=None):
350 """Saves the playlist in a XML file.
353 with open(self.path, 'w') as f:
354 f.write(self.flatten())
355 self._digest = self.digest()