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.
58 item = self[self._index]
59 self._setup_item(item)
62 def jump(self, index):
66 self._index = index % len(self)
69 self.jump(self._index + 1)
72 self.jump(self._index - 1)
74 def filter(self, keeper_fn=lambda item:True, *args, **kwargs):
75 c = copy.deepcopy(self)
76 for item in reversed(c):
78 if keeper_fn(item, *args, **kwargs) != True:
80 try: # attempt to maintain the same current item
81 c._index = c.index(self.current())
86 class Playlist (NoteIndexList):
87 """A :class:`NoteIndexList` of :class:`hooke.curve.Curve`\s.
89 Keeps a list of :attr:`drivers` for loading curves.
91 def __init__(self, drivers, name=None):
92 super(Playlist, self).__init__(name=name)
93 self.drivers = drivers
94 self._loaded = [] # List of loaded curves, see :meth:`._setup_item`.
95 self._max_loaded = 100 # curves to hold in memory simultaneously.
97 def append_curve_by_path(self, path, info=None, identify=True):
99 path = os.path.join(os.path.dirname(self.path), path)
100 path = os.path.normpath(path)
101 c = curve.Curve(path, info=info)
103 c.identify(self.drivers)
107 def _setup_item(self, curve):
108 if curve != None and curve not in self._loaded:
109 if curve not in self:
111 if curve.driver == None:
112 c.identify(self.drivers)
113 if curve.data == None:
115 self._loaded.append(curve)
116 if len(self._loaded) > self._max_loaded:
117 oldest = self._loaded.pop(0)
120 class FilePlaylist (Playlist):
123 def __init__(self, drivers, name=None, path=None):
124 super(FilePlaylist, self).__init__(drivers, name)
127 self._ignored_keys = [
128 'experiment', # class instance, not very exciting.
131 def set_path(self, path):
133 if not path.endswith('.hkp'):
136 if self.name == None:
137 self.name = os.path.basename(path)
140 return self.digest() == self._digest
143 r"""Compute the sha1 digest of the flattened playlist
149 >>> root_path = os.path.sep + 'path'
150 >>> p = FilePlaylist(drivers=[],
151 ... path=os.path.join(root_path, 'to','playlist'))
152 >>> p.info['note'] = 'An example playlist'
153 >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'one'))
154 >>> c.info['note'] = 'The first curve'
156 >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'two'))
157 >>> c.info['note'] = 'The second curve'
160 '\\\x14\x87\x88*q\xf8\xaa\xa7\x84f\x82\xa1S>\xfd3+\xd0o'
162 string = self.flatten()
163 return hashlib.sha1(string).digest()
165 def _clean_key(self, key):
166 """Replace spaces in keys with \\u00B7 (middle dot).
168 This character is deemed unlikely to occur in keys to our
169 playlist and curve info dictionaries, while many keys have
172 \\u00B7 is allowed in XML 1.0 as of the 5th edition. See
173 the `4th edition errata`_ for details.
175 .. _4th edition errata:
176 http://www.w3.org/XML/xml-V10-4e-errata#E09
178 return key.replace(' ', u'\u00B7')
180 def _restore_key(self, key):
181 """Restore keys encoded with :meth:`_clean_key`.
183 return key.replace(u'\u00B7', ' ')
185 def flatten(self, absolute_paths=False):
186 """Create a string representation of the playlist.
188 A playlist is an XML document with the following syntax::
190 <?xml version="1.0" encoding="utf-8"?>
191 <playlist attribute="value">
192 <curve path="/my/file/path/"/ attribute="value" ...>
196 Relative paths are interpreted relative to the location of the
202 >>> root_path = os.path.sep + 'path'
203 >>> p = FilePlaylist(drivers=[],
204 ... path=os.path.join(root_path, 'to','playlist'))
205 >>> p.info['note'] = 'An example playlist'
206 >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'one'))
207 >>> c.info['note'] = 'The first curve'
209 >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'two'))
210 >>> c.info['attr with spaces'] = 'The second curve\\nwith endlines'
212 >>> def _print(string):
213 ... escaped_string = unicode(string, 'utf-8').encode('unicode escape')
214 ... print escaped_string.replace('\\\\n', '\\n').replace('\\\\t', '\\t'),
215 >>> _print(p.flatten()) # doctest: +NORMALIZE_WHITESPACE +REPORT_UDIFF
216 <?xml version="1.0" encoding="utf-8"?>
217 <playlist index="0" note="An example playlist" version="0.1">
218 <curve note="The first curve" path="curve/one"/>
219 <curve attr\\xb7with\\xb7spaces="The second curve
with endlines" path="curve/two"/>
221 >>> _print(p.flatten(absolute_paths=True)) # doctest: +NORMALIZE_WHITESPACE +REPORT_UDIFF
222 <?xml version="1.0" encoding="utf-8"?>
223 <playlist index="0" note="An example playlist" version="0.1">
224 <curve note="The first curve" path="/path/to/curve/one"/>
225 <curve attr\\xb7with\\xb7spaces="The second curve
with endlines" path="/path/to/curve/two"/>
228 implementation = xml.dom.minidom.getDOMImplementation()
229 # create the document DOM object and the root element
230 doc = implementation.createDocument(None, 'playlist', None)
231 root = doc.documentElement
232 root.setAttribute('version', self.version) # store playlist version
233 root.setAttribute('index', str(self._index))
234 for key,value in self.info.items(): # save info variables
235 if (key in self._ignored_keys
236 or not isinstance(value, types.StringTypes)):
238 root.setAttribute(self._clean_key(key), str(value))
239 for curve in self: # save curves and their attributes
240 curve_element = doc.createElement('curve')
241 root.appendChild(curve_element)
242 path = os.path.abspath(os.path.expanduser(curve.path))
243 if absolute_paths == False:
244 path = os.path.relpath(
248 os.path.expanduser(self.path))))
249 curve_element.setAttribute('path', path)
250 for key,value in curve.info.items():
251 if (key in self._ignored_keys
252 or not isinstance(value, types.StringTypes)):
254 curve_element.setAttribute(self._clean_key(key), str(value))
255 string = doc.toprettyxml(encoding='utf-8')
256 root.unlink() # break circular references for garbage collection
259 def _from_xml_doc(self, doc, identify=True):
260 """Load a playlist from an :class:`xml.dom.minidom.Document`
263 root = doc.documentElement
264 for attribute,value in root.attributes.items():
265 attribute = self._restore_key(attribute)
266 if attribute == 'version':
267 assert value == self.version, \
268 'Cannot read v%s playlist with a v%s reader' \
269 % (value, self.version)
270 elif attribute == 'index':
271 self._index = int(value)
273 self.info[attribute] = value
274 for curve_element in doc.getElementsByTagName('curve'):
275 path = curve_element.getAttribute('path')
276 info = dict([(self._restore_key(key), value)
277 for key,value in curve_element.attributes.items()])
279 self.append_curve_by_path(path, info, identify=identify)
280 self.jump(self._index) # ensure valid index
282 def from_string(self, string, identify=True):
283 u"""Load a playlist from a string.
288 >>> string = '''<?xml version="1.0" encoding="utf-8"?>
289 ... <playlist index="1" note="An example playlist" version="0.1">
290 ... <curve note="The first curve" path="../curve/one"/>
291 ... <curve attr\xb7with\xb7spaces="The second curve
with endlines" path="../curve/two"/>
294 >>> p = FilePlaylist(drivers=[],
295 ... path=os.path.join('path', 'to', 'my', 'playlist'))
296 >>> p.from_string(string, identify=False)
300 {u'note': u'An example playlist'}
305 >>> p[-1].info['attr with spaces']
306 u'The second curve\\nwith endlines'
308 doc = xml.dom.minidom.parseString(string)
309 self._from_xml_doc(doc, identify=identify)
311 def load(self, path=None, identify=True):
312 """Load a playlist from a file.
315 doc = xml.dom.minidom.parse(self.path)
316 self._from_xml_doc(doc, identify=identify)
317 self._digest = self.digest()
319 def save(self, path=None):
320 """Saves the playlist in a XML file.
323 f = file(self.path, 'w')
324 f.write(self.flatten())