"""The `playlist` module provides a :class:`Playlist` and 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 NoteIndexList (list):
"""A list that keeps track of a "current" item and additional notes.
:attr:`index` (i.e. "bookmark") is the index of the currently
current curve. Also keep a :class:`dict` of additional information
(:attr:`info`).
"""
def __init__(self, name=None):
super(NoteIndexList, self).__init__()
self.name = name
self.info = {}
self._index = 0
def current(self):
if len(self) == 0:
return None
return self[self._index]
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 item:True):
c = copy.deepcopy(self)
for item in reversed(c):
if keeper_fn(item) != True:
c.remove(item)
try: # attempt to maintain the same current item
c._index = c.index(self.current())
except ValueError:
c._index = 0
return c
class Playlist (NoteIndexList):
"""A :class:`NoteIndexList` of :class:`hooke.curve.Curve`\s.
Keeps a list of :attr:`drivers` for loading curves.
"""
def __init__(self, drivers, name=None):
super(Playlist, self).__init__(name=name)
self.drivers = drivers
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
class FilePlaylist (Playlist):
version = '0.1'
def __init__(self, drivers, name=None, path=None):
super(FilePlaylist, self).__init__(drivers, name)
self.set_path(path)
self._digest = None
def set_path(self, path):
if path != None:
if not path.endswith('.hkp'):
path += '.hkp'
self.path = path
if self.name == None:
self.name = os.path.basename(path)
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::
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
>>> print p.flatten(absolute_paths=True) # doctest: +NORMALIZE_WHITESPACE +REPORT_UDIFF
"""
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 = '''
...
...
...
...
... '''
>>> 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.
"""
self.set_path(path)
doc = xml.dom.minidom.parse(self.path)
return self._from_xml_doc(doc)
def save(self, path=None):
"""Saves the playlist in a XML file.
"""
self.set_path(path)
f = file(self.path, 'w')
f.write(self.flatten())
f.close()