--- /dev/null
+"""The playlist module provides :class:`Playlist` its subclass
+:class:`FilePlaylist` for manipulating lists of
+:class:`hooke.curve.Curve`\s.
+"""
+
+import copy
+import hashlib
+import os.path
+import xml.dom.minidom
+
+from . import curve as curve
+
+class Playlist (list):
+ """A list of :class:`hooke.curve.Curve`\s.
+
+ Keeps a list of :attr:`drivers` for loading curves, the
+ :attr:`index` (i.e. "bookmark") of the currently active curve, and
+ a :class:`dict` of additional informtion (:attr:`info`).
+ """
+ def __init__(self, drivers, name=None):
+ super(Playlist, self).__init__()
+ self.drivers = drivers
+ self.name = name
+ self.info = {}
+ self._index = 0
+
+ def append_curve_by_path(self, path, info=None, identify=True):
+ if self.path != None:
+ path = os.path.join(self.path, path)
+ path = os.path.normpath(path)
+ c = curve.Curve(path, info=info)
+ if identify == True:
+ c.identify(self.drivers)
+ self.append(c)
+ return c
+
+ def active_curve(self):
+ return self[self._index]
+
+ def has_curves(self):
+ return len(self) > 0
+
+ def jump(self, index):
+ if len(self) == 0:
+ self._index = 0
+ else:
+ self._index = index % len(self)
+
+ def next(self):
+ self.jump(self._index + 1)
+
+ def previous(self):
+ self.jump(self._index - 1)
+
+ def filter(self, keeper_fn=lambda curve:True):
+ playlist = copy.deepcopy(self)
+ for curve in reversed(playlist.curves):
+ if keeper_fn(curve) != True:
+ playlist.curves.remove(curve)
+ try: # attempt to maintain the same active curve
+ playlist._index = playlist.index(self.active_curve())
+ except ValueError:
+ playlist._index = 0
+ return playlist
+
+class FilePlaylist (Playlist):
+ version = '0.1'
+
+ def __init__(self, drivers, name=None, path=None):
+ if name == None and path != None:
+ name = os.path.basename(path)
+ super(FilePlaylist, self).__init__(drivers, name)
+ self.path = path
+ self._digest = None
+
+ def is_saved(self):
+ return self.digest() == self._digest
+
+ def digest(self):
+ r"""Compute the sha1 digest of the flattened playlist
+ representation.
+
+ Examples
+ --------
+
+ >>> root_path = os.path.sep + 'path'
+ >>> p = FilePlaylist(drivers=[],
+ ... path=os.path.join(root_path, 'to','playlist'))
+ >>> p.info['note'] = 'An example playlist'
+ >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'one'))
+ >>> c.info['note'] = 'The first curve'
+ >>> p.append(c)
+ >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'two'))
+ >>> c.info['note'] = 'The second curve'
+ >>> p.append(c)
+ >>> p.digest()
+ "\xa1\x99\x8a\x99\xed\xad\x13'\xa7w\x12\x00\x07Z\xb3\xd0zN\xa2\xe1"
+ """
+ string = self.flatten()
+ return hashlib.sha1(string).digest()
+
+ def flatten(self, absolute_paths=False):
+ """Create a string representation of the playlist.
+
+ A playlist is an XML document with the following syntax::
+
+ <?xml version="1.0" encoding="utf-8"?>
+ <playlist attribute="value">
+ <curve path="/my/file/path/"/ attribute="value" ...>
+ <curve path="...">
+ </playlist>
+
+ Relative paths are interpreted relative to the location of the
+ playlist file.
+
+ Examples
+ --------
+
+ >>> root_path = os.path.sep + 'path'
+ >>> p = FilePlaylist(drivers=[],
+ ... path=os.path.join(root_path, 'to','playlist'))
+ >>> p.info['note'] = 'An example playlist'
+ >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'one'))
+ >>> c.info['note'] = 'The first curve'
+ >>> p.append(c)
+ >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'two'))
+ >>> c.info['note'] = 'The second curve'
+ >>> p.append(c)
+ >>> print p.flatten() # doctest: +NORMALIZE_WHITESPACE +REPORT_UDIFF
+ <?xml version="1.0" encoding="utf-8"?>
+ <playlist index="0" note="An example playlist" version="0.1">
+ <curve note="The first curve" path="../curve/one"/>
+ <curve note="The second curve" path="../curve/two"/>
+ </playlist>
+ <BLANKLINE>
+ >>> print p.flatten(absolute_paths=True) # doctest: +NORMALIZE_WHITESPACE +REPORT_UDIFF
+ <?xml version="1.0" encoding="utf-8"?>
+ <playlist index="0" note="An example playlist" version="0.1">
+ <curve note="The first curve" path="/path/to/curve/one"/>
+ <curve note="The second curve" path="/path/to/curve/two"/>
+ </playlist>
+ <BLANKLINE>
+ """
+ implementation = xml.dom.minidom.getDOMImplementation()
+ # create the document DOM object and the root element
+ doc = implementation.createDocument(None, 'playlist', None)
+ root = doc.documentElement
+ root.setAttribute('version', self.version) # store playlist version
+ root.setAttribute('index', str(self._index))
+ for key,value in self.info.items(): # save info variables
+ root.setAttribute(key, str(value))
+ for curve in self: # save curves and their attributes
+ curve_element = doc.createElement('curve')
+ root.appendChild(curve_element)
+ path = os.path.abspath(os.path.expanduser(curve.path))
+ if absolute_paths == False:
+ path = os.path.relpath(
+ path,
+ os.path.abspath(os.path.expanduser(self.path)))
+ curve_element.setAttribute('path', path)
+ for key,value in curve.info.items():
+ curve_element.setAttribute(key, str(value))
+ string = doc.toprettyxml(encoding='utf-8')
+ root.unlink() # break circular references for garbage collection
+ return string
+
+ def _from_xml_doc(self, doc):
+ """Load a playlist from an :class:`xml.dom.minidom.Document`
+ instance.
+ """
+ root = doc.documentElement
+ for attribute,value in root.attributes.items():
+ if attribute == 'version':
+ assert value == self.version, \
+ 'Cannot read v%s playlist with a v%s reader' \
+ % (value, self.version)
+ elif attribute == 'index':
+ self._index = int(value)
+ else:
+ self.info[attribute] = value
+ for curve_element in doc.getElementsByTagName('curve'):
+ path = curve_element.getAttribute('path')
+ info = dict(curve_element.attributes.items())
+ info.pop('path')
+ self.append_curve_by_path(path, info, identify=False)
+ self.jump(self._index) # ensure valid index
+
+ def from_string(self, string):
+ """Load a playlist from a string.
+
+ Examples
+ --------
+
+ >>> string = '''<?xml version="1.0" encoding="utf-8"?>
+ ... <playlist index="1" note="An example playlist" version="0.1">
+ ... <curve note="The first curve" path="../curve/one"/>
+ ... <curve note="The second curve" path="../curve/two"/>
+ ... </playlist>
+ ... '''
+ >>> p = FilePlaylist(drivers=[],
+ ... path=os.path.join('path', 'to','playlist'))
+ >>> p.from_string(string)
+ >>> p._index
+ 1
+ >>> p.info
+ {u'note': u'An example playlist'}
+ >>> for curve in p:
+ ... print curve.path
+ path/to/curve/one
+ path/to/curve/two
+ """
+ doc = xml.dom.minidom.parseString(string)
+ return self._from_xml_doc(doc)
+
+ def load(self, path=None):
+ """Load a playlist from a file.
+ """
+ if path != None:
+ self.path = path
+ if self.name == None:
+ self.name = os.path.basename(self.path)
+ doc = xml.dom.minidom.parse(path)
+ return self._from_xml_doc(doc)
+
+ def save(self, path):
+ """Saves the playlist in a XML file.
+ """
+ f = file(path, 'w')
+ f.write(self.flatten())
+ f.close()