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 '<%s %s>' % (self.__class__.__name__, self.name)
50 def _setup_item(self, item):
51 """Perform any required initialization before returning an item.
61 item = self[self._index]
62 self._setup_item(item)
65 def jump(self, index):
69 self._index = index % len(self)
72 self.jump(self._index + 1)
75 self.jump(self._index - 1)
77 def filter(self, keeper_fn=lambda item:True, *args, **kwargs):
78 c = copy.deepcopy(self)
79 for item in reversed(c):
81 if keeper_fn(item, *args, **kwargs) != True:
83 try: # attempt to maintain the same current item
84 c._index = c.index(self.current())
89 class Playlist (NoteIndexList):
90 """A :class:`NoteIndexList` of :class:`hooke.curve.Curve`\s.
92 Keeps a list of :attr:`drivers` for loading curves.
94 def __init__(self, drivers, name=None):
95 super(Playlist, self).__init__(name=name)
96 self.drivers = drivers
97 self._loaded = [] # List of loaded curves, see :meth:`._setup_item`.
98 self._max_loaded = 100 # curves to hold in memory simultaneously.
100 def append_curve_by_path(self, path, info=None, identify=True):
101 if self.path != None:
102 path = os.path.join(os.path.dirname(self.path), path)
103 path = os.path.normpath(path)
104 c = curve.Curve(path, info=info)
106 c.identify(self.drivers)
110 def _setup_item(self, curve):
111 if curve != None and curve not in self._loaded:
112 if curve not in self:
114 if curve.driver == None:
115 c.identify(self.drivers)
116 if curve.data == None:
118 self._loaded.append(curve)
119 if len(self._loaded) > self._max_loaded:
120 oldest = self._loaded.pop(0)
123 class FilePlaylist (Playlist):
126 def __init__(self, drivers, name=None, path=None):
127 super(FilePlaylist, self).__init__(drivers, name)
130 self._ignored_keys = [
131 'experiment', # class instance, not very exciting.
134 def set_path(self, path):
136 if not path.endswith('.hkp'):
139 if self.name == None:
140 self.name = os.path.basename(path)
143 return self.digest() == self._digest
146 r"""Compute the sha1 digest of the flattened playlist
152 >>> root_path = os.path.sep + 'path'
153 >>> p = FilePlaylist(drivers=[],
154 ... path=os.path.join(root_path, 'to','playlist'))
155 >>> p.info['note'] = 'An example playlist'
156 >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'one'))
157 >>> c.info['note'] = 'The first curve'
159 >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'two'))
160 >>> c.info['note'] = 'The second curve'
163 '\\\x14\x87\x88*q\xf8\xaa\xa7\x84f\x82\xa1S>\xfd3+\xd0o'
165 string = self.flatten()
166 return hashlib.sha1(string).digest()
168 def _clean_key(self, key):
169 """Replace spaces in keys with \\u00B7 (middle dot).
171 This character is deemed unlikely to occur in keys to our
172 playlist and curve info dictionaries, while many keys have
175 \\u00B7 is allowed in XML 1.0 as of the 5th edition. See
176 the `4th edition errata`_ for details.
178 .. _4th edition errata:
179 http://www.w3.org/XML/xml-V10-4e-errata#E09
181 return key.replace(' ', u'\u00B7')
183 def _restore_key(self, key):
184 """Restore keys encoded with :meth:`_clean_key`.
186 return key.replace(u'\u00B7', ' ')
188 def flatten(self, absolute_paths=False):
189 """Create a string representation of the playlist.
191 A playlist is an XML document with the following syntax::
193 <?xml version="1.0" encoding="utf-8"?>
194 <playlist attribute="value">
195 <curve path="/my/file/path/"/ attribute="value" ...>
199 Relative paths are interpreted relative to the location of the
205 >>> root_path = os.path.sep + 'path'
206 >>> p = FilePlaylist(drivers=[],
207 ... path=os.path.join(root_path, 'to','playlist'))
208 >>> p.info['note'] = 'An example playlist'
209 >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'one'))
210 >>> c.info['note'] = 'The first curve'
212 >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'two'))
213 >>> c.info['attr with spaces'] = 'The second curve\\nwith endlines'
215 >>> def _print(string):
216 ... escaped_string = unicode(string, 'utf-8').encode('unicode escape')
217 ... print escaped_string.replace('\\\\n', '\\n').replace('\\\\t', '\\t'),
218 >>> _print(p.flatten()) # doctest: +NORMALIZE_WHITESPACE +REPORT_UDIFF
219 <?xml version="1.0" encoding="utf-8"?>
220 <playlist index="0" note="An example playlist" version="0.1">
221 <curve note="The first curve" path="curve/one"/>
222 <curve attr\\xb7with\\xb7spaces="The second curve
with endlines" path="curve/two"/>
224 >>> _print(p.flatten(absolute_paths=True)) # doctest: +NORMALIZE_WHITESPACE +REPORT_UDIFF
225 <?xml version="1.0" encoding="utf-8"?>
226 <playlist index="0" note="An example playlist" version="0.1">
227 <curve note="The first curve" path="/path/to/curve/one"/>
228 <curve attr\\xb7with\\xb7spaces="The second curve
with endlines" path="/path/to/curve/two"/>
231 implementation = xml.dom.minidom.getDOMImplementation()
232 # create the document DOM object and the root element
233 doc = implementation.createDocument(None, 'playlist', None)
234 root = doc.documentElement
235 root.setAttribute('version', self.version) # store playlist version
236 root.setAttribute('index', str(self._index))
237 for key,value in self.info.items(): # save info variables
238 if (key in self._ignored_keys
239 or not isinstance(value, types.StringTypes)):
241 root.setAttribute(self._clean_key(key), str(value))
242 for curve in self: # save curves and their attributes
243 curve_element = doc.createElement('curve')
244 root.appendChild(curve_element)
245 path = os.path.abspath(os.path.expanduser(curve.path))
246 if absolute_paths == False:
247 path = os.path.relpath(
251 os.path.expanduser(self.path))))
252 curve_element.setAttribute('path', path)
253 for key,value in curve.info.items():
254 if (key in self._ignored_keys
255 or not isinstance(value, types.StringTypes)):
257 curve_element.setAttribute(self._clean_key(key), str(value))
258 string = doc.toprettyxml(encoding='utf-8')
259 root.unlink() # break circular references for garbage collection
262 def _from_xml_doc(self, doc, identify=True):
263 """Load a playlist from an :class:`xml.dom.minidom.Document`
266 root = doc.documentElement
267 for attribute,value in root.attributes.items():
268 attribute = self._restore_key(attribute)
269 if attribute == 'version':
270 assert value == self.version, \
271 'Cannot read v%s playlist with a v%s reader' \
272 % (value, self.version)
273 elif attribute == 'index':
274 self._index = int(value)
276 self.info[attribute] = value
277 for curve_element in doc.getElementsByTagName('curve'):
278 path = curve_element.getAttribute('path')
279 info = dict([(self._restore_key(key), value)
280 for key,value in curve_element.attributes.items()])
282 self.append_curve_by_path(path, info, identify=identify)
283 self.jump(self._index) # ensure valid index
285 def from_string(self, string, identify=True):
286 u"""Load a playlist from a string.
291 >>> string = '''<?xml version="1.0" encoding="utf-8"?>
292 ... <playlist index="1" note="An example playlist" version="0.1">
293 ... <curve note="The first curve" path="../curve/one"/>
294 ... <curve attr\xb7with\xb7spaces="The second curve
with endlines" path="../curve/two"/>
297 >>> p = FilePlaylist(drivers=[],
298 ... path=os.path.join('path', 'to', 'my', 'playlist'))
299 >>> p.from_string(string, identify=False)
303 {u'note': u'An example playlist'}
308 >>> p[-1].info['attr with spaces']
309 u'The second curve\\nwith endlines'
311 doc = xml.dom.minidom.parseString(string)
312 self._from_xml_doc(doc, identify=identify)
314 def load(self, path=None, identify=True):
315 """Load a playlist from a file.
318 doc = xml.dom.minidom.parse(self.path)
319 self._from_xml_doc(doc, identify=identify)
320 self._digest = self.digest()
322 def save(self, path=None):
323 """Saves the playlist in a XML file.
326 f = file(self.path, 'w')
327 f.write(self.flatten())