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.
55 def index(self, value=None, *args, **kwargs):
56 """Extend `list.index`, returning the current index if `value`
61 return super(NoteIndexList, self).index(value, *args, **kwargs)
66 item = self[self._index]
67 self._setup_item(item)
70 def jump(self, index):
74 self._index = index % len(self)
77 self.jump(self._index + 1)
80 self.jump(self._index - 1)
82 def items(self, reverse=False):
83 """Iterate through `self` calling `_setup_item` on each item
88 items = reversed(self)
90 self._setup_item(item)
93 def filter(self, keeper_fn=lambda item:True, *args, **kwargs):
94 c = copy.deepcopy(self)
95 for item in c.items(reverse=True):
96 if keeper_fn(item, *args, **kwargs) != True:
98 try: # attempt to maintain the same current item
99 c._index = c.index(self.current())
104 class Playlist (NoteIndexList):
105 """A :class:`NoteIndexList` of :class:`hooke.curve.Curve`\s.
107 Keeps a list of :attr:`drivers` for loading curves.
109 def __init__(self, drivers, name=None):
110 super(Playlist, self).__init__(name=name)
111 self.drivers = drivers
112 self._loaded = [] # List of loaded curves, see :meth:`._setup_item`.
113 self._max_loaded = 100 # curves to hold in memory simultaneously.
115 def append_curve_by_path(self, path, info=None, identify=True):
116 if self.path != None:
117 path = os.path.join(os.path.dirname(self.path), path)
118 path = os.path.normpath(path)
119 c = curve.Curve(path, info=info)
121 c.identify(self.drivers)
125 def _setup_item(self, curve):
126 if curve != None and curve not in self._loaded:
127 if curve not in self:
129 if curve.driver == None:
130 c.identify(self.drivers)
131 if curve.data == None:
133 self._loaded.append(curve)
134 if len(self._loaded) > self._max_loaded:
135 oldest = self._loaded.pop(0)
138 class FilePlaylist (Playlist):
141 def __init__(self, drivers, name=None, path=None):
142 super(FilePlaylist, self).__init__(drivers, name)
145 self._ignored_keys = [
146 'experiment', # class instance, not very exciting.
149 def set_path(self, path):
151 if not path.endswith('.hkp'):
154 if self.name == None:
155 self.name = os.path.basename(path)
158 return self.digest() == self._digest
161 r"""Compute the sha1 digest of the flattened playlist
167 >>> root_path = os.path.sep + 'path'
168 >>> p = FilePlaylist(drivers=[],
169 ... path=os.path.join(root_path, 'to','playlist'))
170 >>> p.info['note'] = 'An example playlist'
171 >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'one'))
172 >>> c.info['note'] = 'The first curve'
174 >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'two'))
175 >>> c.info['note'] = 'The second curve'
178 '\\\x14\x87\x88*q\xf8\xaa\xa7\x84f\x82\xa1S>\xfd3+\xd0o'
180 string = self.flatten()
181 return hashlib.sha1(string).digest()
183 def _clean_key(self, key):
184 """Replace spaces in keys with \\u00B7 (middle dot).
186 This character is deemed unlikely to occur in keys to our
187 playlist and curve info dictionaries, while many keys have
190 \\u00B7 is allowed in XML 1.0 as of the 5th edition. See
191 the `4th edition errata`_ for details.
193 .. _4th edition errata:
194 http://www.w3.org/XML/xml-V10-4e-errata#E09
196 return key.replace(' ', u'\u00B7')
198 def _restore_key(self, key):
199 """Restore keys encoded with :meth:`_clean_key`.
201 return key.replace(u'\u00B7', ' ')
203 def flatten(self, absolute_paths=False):
204 """Create a string representation of the playlist.
206 A playlist is an XML document with the following syntax::
208 <?xml version="1.0" encoding="utf-8"?>
209 <playlist attribute="value">
210 <curve path="/my/file/path/"/ attribute="value" ...>
214 Relative paths are interpreted relative to the location of the
220 >>> root_path = os.path.sep + 'path'
221 >>> p = FilePlaylist(drivers=[],
222 ... path=os.path.join(root_path, 'to','playlist'))
223 >>> p.info['note'] = 'An example playlist'
224 >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'one'))
225 >>> c.info['note'] = 'The first curve'
227 >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'two'))
228 >>> c.info['attr with spaces'] = 'The second curve\\nwith endlines'
230 >>> def _print(string):
231 ... escaped_string = unicode(string, 'utf-8').encode('unicode escape')
232 ... print escaped_string.replace('\\\\n', '\\n').replace('\\\\t', '\\t'),
233 >>> _print(p.flatten()) # doctest: +NORMALIZE_WHITESPACE +REPORT_UDIFF
234 <?xml version="1.0" encoding="utf-8"?>
235 <playlist index="0" note="An example playlist" version="0.1">
236 <curve note="The first curve" path="curve/one"/>
237 <curve attr\\xb7with\\xb7spaces="The second curve
with endlines" path="curve/two"/>
239 >>> _print(p.flatten(absolute_paths=True)) # doctest: +NORMALIZE_WHITESPACE +REPORT_UDIFF
240 <?xml version="1.0" encoding="utf-8"?>
241 <playlist index="0" note="An example playlist" version="0.1">
242 <curve note="The first curve" path="/path/to/curve/one"/>
243 <curve attr\\xb7with\\xb7spaces="The second curve
with endlines" path="/path/to/curve/two"/>
246 implementation = xml.dom.minidom.getDOMImplementation()
247 # create the document DOM object and the root element
248 doc = implementation.createDocument(None, 'playlist', None)
249 root = doc.documentElement
250 root.setAttribute('version', self.version) # store playlist version
251 root.setAttribute('index', str(self._index))
252 for key,value in self.info.items(): # save info variables
253 if (key in self._ignored_keys
254 or not isinstance(value, types.StringTypes)):
256 root.setAttribute(self._clean_key(key), str(value))
257 for curve in self: # save curves and their attributes
258 curve_element = doc.createElement('curve')
259 root.appendChild(curve_element)
260 path = os.path.abspath(os.path.expanduser(curve.path))
261 if absolute_paths == False:
262 path = os.path.relpath(
266 os.path.expanduser(self.path))))
267 curve_element.setAttribute('path', path)
268 for key,value in curve.info.items():
269 if (key in self._ignored_keys
270 or not isinstance(value, types.StringTypes)):
272 curve_element.setAttribute(self._clean_key(key), str(value))
273 string = doc.toprettyxml(encoding='utf-8')
274 root.unlink() # break circular references for garbage collection
277 def _from_xml_doc(self, doc, identify=True):
278 """Load a playlist from an :class:`xml.dom.minidom.Document`
281 root = doc.documentElement
282 for attribute,value in root.attributes.items():
283 attribute = self._restore_key(attribute)
284 if attribute == 'version':
285 assert value == self.version, \
286 'Cannot read v%s playlist with a v%s reader' \
287 % (value, self.version)
288 elif attribute == 'index':
289 self._index = int(value)
291 self.info[attribute] = value
292 for curve_element in doc.getElementsByTagName('curve'):
293 path = curve_element.getAttribute('path')
294 info = dict([(self._restore_key(key), value)
295 for key,value in curve_element.attributes.items()])
297 self.append_curve_by_path(path, info, identify=identify)
298 self.jump(self._index) # ensure valid index
300 def from_string(self, string, identify=True):
301 u"""Load a playlist from a string.
306 >>> string = '''<?xml version="1.0" encoding="utf-8"?>
307 ... <playlist index="1" note="An example playlist" version="0.1">
308 ... <curve note="The first curve" path="../curve/one"/>
309 ... <curve attr\xb7with\xb7spaces="The second curve
with endlines" path="../curve/two"/>
312 >>> p = FilePlaylist(drivers=[],
313 ... path=os.path.join('path', 'to', 'my', 'playlist'))
314 >>> p.from_string(string, identify=False)
318 {u'note': u'An example playlist'}
323 >>> p[-1].info['attr with spaces']
324 u'The second curve\\nwith endlines'
326 doc = xml.dom.minidom.parseString(string)
327 self._from_xml_doc(doc, identify=identify)
329 def load(self, path=None, identify=True):
330 """Load a playlist from a file.
333 doc = xml.dom.minidom.parse(self.path)
334 self._from_xml_doc(doc, identify=identify)
335 self._digest = self.digest()
337 def save(self, path=None):
338 """Saves the playlist in a XML file.
341 f = file(self.path, 'w')
342 f.write(self.flatten())