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())
110 class Playlist (NoteIndexList):
111 """A :class:`NoteIndexList` of :class:`hooke.curve.Curve`\s.
113 Keeps a list of :attr:`drivers` for loading curves.
115 def __init__(self, drivers, name=None):
116 super(Playlist, self).__init__(name=name)
117 self.drivers = drivers
118 self._loaded = [] # List of loaded curves, see :meth:`._setup_item`.
119 self._max_loaded = 100 # curves to hold in memory simultaneously.
121 def append_curve_by_path(self, path, info=None, identify=True):
122 path = os.path.normpath(path)
123 c = curve.Curve(path, info=info)
125 c.identify(self.drivers)
129 def _setup_item(self, curve):
130 if curve != None and curve not in self._loaded:
131 if curve not in self:
133 if curve.driver == None:
134 c.identify(self.drivers)
135 if curve.data == None:
137 self._loaded.append(curve)
138 if len(self._loaded) > self._max_loaded:
139 oldest = self._loaded.pop(0)
142 class FilePlaylist (Playlist):
145 def __init__(self, drivers, name=None, path=None):
146 super(FilePlaylist, self).__init__(drivers, name)
150 self._ignored_keys = [
151 'experiment', # class instance, not very exciting.
154 def set_path(self, path):
156 if not path.endswith('.hkp'):
159 if self.name == None:
160 self.name = os.path.basename(path)
162 def append_curve_by_path(self, path, *args, **kwargs):
163 if self.path != None:
164 path = os.path.join(os.path.dirname(self.path), path)
165 super(FilePlaylist, self).append_curve_by_path(path, *args, **kwargs)
168 return self.digest() == self._digest
171 r"""Compute the sha1 digest of the flattened playlist
177 >>> root_path = os.path.sep + 'path'
178 >>> p = FilePlaylist(drivers=[],
179 ... path=os.path.join(root_path, 'to','playlist'))
180 >>> p.info['note'] = 'An example playlist'
181 >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'one'))
182 >>> c.info['note'] = 'The first curve'
184 >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'two'))
185 >>> c.info['note'] = 'The second curve'
188 '\\\x14\x87\x88*q\xf8\xaa\xa7\x84f\x82\xa1S>\xfd3+\xd0o'
190 string = self.flatten()
191 return hashlib.sha1(string).digest()
193 def _clean_key(self, key):
194 """Replace spaces in keys with \\u00B7 (middle dot).
196 This character is deemed unlikely to occur in keys to our
197 playlist and curve info dictionaries, while many keys have
200 \\u00B7 is allowed in XML 1.0 as of the 5th edition. See
201 the `4th edition errata`_ for details.
203 .. _4th edition errata:
204 http://www.w3.org/XML/xml-V10-4e-errata#E09
206 return key.replace(' ', u'\u00B7')
208 def _restore_key(self, key):
209 """Restore keys encoded with :meth:`_clean_key`.
211 return key.replace(u'\u00B7', ' ')
213 def flatten(self, absolute_paths=False):
214 """Create a string representation of the playlist.
216 A playlist is an XML document with the following syntax::
218 <?xml version="1.0" encoding="utf-8"?>
219 <playlist attribute="value">
220 <curve path="/my/file/path/"/ attribute="value" ...>
224 Relative paths are interpreted relative to the location of the
230 >>> root_path = os.path.sep + 'path'
231 >>> p = FilePlaylist(drivers=[],
232 ... path=os.path.join(root_path, 'to','playlist'))
233 >>> p.info['note'] = 'An example playlist'
234 >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'one'))
235 >>> c.info['note'] = 'The first curve'
237 >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'two'))
238 >>> c.info['attr with spaces'] = 'The second curve\\nwith endlines'
240 >>> def _print(string):
241 ... escaped_string = unicode(string, 'utf-8').encode('unicode escape')
242 ... print escaped_string.replace('\\\\n', '\\n').replace('\\\\t', '\\t'),
243 >>> _print(p.flatten()) # doctest: +NORMALIZE_WHITESPACE +REPORT_UDIFF
244 <?xml version="1.0" encoding="utf-8"?>
245 <playlist index="0" note="An example playlist" version="0.1">
246 <curve note="The first curve" path="curve/one"/>
247 <curve attr\\xb7with\\xb7spaces="The second curve
with endlines" path="curve/two"/>
249 >>> _print(p.flatten(absolute_paths=True)) # doctest: +NORMALIZE_WHITESPACE +REPORT_UDIFF
250 <?xml version="1.0" encoding="utf-8"?>
251 <playlist index="0" note="An example playlist" version="0.1">
252 <curve note="The first curve" path="/path/to/curve/one"/>
253 <curve attr\\xb7with\\xb7spaces="The second curve
with endlines" path="/path/to/curve/two"/>
256 implementation = xml.dom.minidom.getDOMImplementation()
257 # create the document DOM object and the root element
258 doc = implementation.createDocument(None, 'playlist', None)
259 root = doc.documentElement
260 root.setAttribute('version', self.version) # store playlist version
261 root.setAttribute('index', str(self._index))
262 for key,value in self.info.items(): # save info variables
263 if (key in self._ignored_keys
264 or not isinstance(value, types.StringTypes)):
266 root.setAttribute(self._clean_key(key), str(value))
267 for curve in self: # save curves and their attributes
268 curve_element = doc.createElement('curve')
269 root.appendChild(curve_element)
270 path = os.path.abspath(os.path.expanduser(curve.path))
271 if absolute_paths == False:
272 path = os.path.relpath(
276 os.path.expanduser(self.path))))
277 curve_element.setAttribute('path', path)
278 for key,value in curve.info.items():
279 if (key in self._ignored_keys
280 or not isinstance(value, types.StringTypes)):
282 curve_element.setAttribute(self._clean_key(key), str(value))
283 string = doc.toprettyxml(encoding='utf-8')
284 root.unlink() # break circular references for garbage collection
287 def _from_xml_doc(self, doc, identify=True):
288 """Load a playlist from an :class:`xml.dom.minidom.Document`
291 root = doc.documentElement
292 for attribute,value in root.attributes.items():
293 attribute = self._restore_key(attribute)
294 if attribute == 'version':
295 assert value == self.version, \
296 'Cannot read v%s playlist with a v%s reader' \
297 % (value, self.version)
298 elif attribute == 'index':
299 self._index = int(value)
301 self.info[attribute] = value
302 for curve_element in doc.getElementsByTagName('curve'):
303 path = curve_element.getAttribute('path')
304 info = dict([(self._restore_key(key), value)
305 for key,value in curve_element.attributes.items()])
307 self.append_curve_by_path(path, info, identify=identify)
308 self.jump(self._index) # ensure valid index
310 def from_string(self, string, identify=True):
311 u"""Load a playlist from a string.
316 >>> string = '''<?xml version="1.0" encoding="utf-8"?>
317 ... <playlist index="1" note="An example playlist" version="0.1">
318 ... <curve note="The first curve" path="../curve/one"/>
319 ... <curve attr\xb7with\xb7spaces="The second curve
with endlines" path="../curve/two"/>
322 >>> p = FilePlaylist(drivers=[],
323 ... path=os.path.join('path', 'to', 'my', 'playlist'))
324 >>> p.from_string(string, identify=False)
328 {u'note': u'An example playlist'}
333 >>> p[-1].info['attr with spaces']
334 u'The second curve\\nwith endlines'
336 doc = xml.dom.minidom.parseString(string)
337 self._from_xml_doc(doc, identify=identify)
339 def load(self, path=None, identify=True):
340 """Load a playlist from a file.
343 doc = xml.dom.minidom.parse(self.path)
344 self._from_xml_doc(doc, identify=identify)
345 self._digest = self.digest()
347 def save(self, path=None):
348 """Saves the playlist in a XML file.
351 f = file(self.path, 'w')
352 f.write(self.flatten())