X-Git-Url: http://git.tremily.us/?a=blobdiff_plain;f=hooke%2Fplaylist.py;h=8444c9aac883f151e274a9810e7ef7ee4976326f;hb=e19b115bbff9515313d4b2660d530556e42f3c0d;hp=35bf54781f8c1d887190fc672728e92b9ef89d43;hpb=094950c08f96c0f3c5bff65e03dc139c482bb9d4;p=hooke.git diff --git a/hooke/playlist.py b/hooke/playlist.py index 35bf547..8444c9a 100644 --- a/hooke/playlist.py +++ b/hooke/playlist.py @@ -1,4 +1,22 @@ -"""The playlist module provides :class:`Playlist` its subclass +# Copyright (C) 2010 W. Trevor King +# +# This file is part of Hooke. +# +# Hooke is free software: you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# Hooke is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General +# Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with Hooke. If not, see +# . + +"""The `playlist` module provides a :class:`Playlist` and its subclass :class:`FilePlaylist` for manipulating lists of :class:`hooke.curve.Curve`\s. """ @@ -6,39 +24,48 @@ import copy import hashlib import os.path +import types import xml.dom.minidom from . import curve as curve +from .compat import minidom as minidom # dynamically patch xml.sax.minidom + -class Playlist (list): - """A list of :class:`hooke.curve.Curve`\s. +class NoteIndexList (list): + """A list that keeps track of a "current" item and additional notes. - 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`). + :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, drivers, name=None): - super(Playlist, self).__init__() - self.drivers = drivers + def __init__(self, name=None): + super(NoteIndexList, self).__init__() 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 __str__(self): + return '<%s %s>' % (self.__class__.__name__, self.name) - def active_curve(self): - return self[self._index] + def _setup_item(self, item): + """Perform any required initialization before returning an item. + """ + pass - def has_curves(self): - return len(self) > 0 + def index(self, value=None, *args, **kwargs): + """Extend `list.index`, returning the current index if `value` + is `None`. + """ + if value == None: + return self._index + return super(NoteIndexList, self).index(value, *args, **kwargs) + + def current(self): + if len(self) == 0: + return None + item = self[self._index] + self._setup_item(item) + return item def jump(self, index): if len(self) == 0: @@ -52,17 +79,61 @@ class Playlist (list): 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()) + def items(self, reverse=False): + """Iterate through `self` calling `_setup_item` on each item + before yielding. + """ + items = self + if reverse == True: + items = reversed(self) + for item in items: + self._setup_item(item) + yield item + + def filter(self, keeper_fn=lambda item:True, *args, **kwargs): + c = copy.deepcopy(self) + for item in c.items(reverse=True): + if keeper_fn(item, *args, **kwargs) != True: + c.remove(item) + try: # attempt to maintain the same current item + c._index = c.index(self.current()) except ValueError: - playlist._index = 0 - return playlist + 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 + self._loaded = [] # List of loaded curves, see :meth:`._setup_item`. + self._max_loaded = 100 # curves to hold in memory simultaneously. + + def append_curve_by_path(self, path, info=None, identify=True): + if self.path != None: + path = os.path.join(os.path.dirname(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 _setup_item(self, curve): + if curve != None and curve not in self._loaded: + if curve not in self: + self.append(curve) + if curve.driver == None: + c.identify(self.drivers) + if curve.data == None: + curve.load() + self._loaded.append(curve) + if len(self._loaded) > self._max_loaded: + oldest = self._loaded.pop(0) + oldest.unload() class FilePlaylist (Playlist): version = '0.1' @@ -71,12 +142,17 @@ class FilePlaylist (Playlist): super(FilePlaylist, self).__init__(drivers, name) self.set_path(path) self._digest = None + self._ignored_keys = [ + 'experiment', # class instance, not very exciting. + ] def set_path(self, path): - if not path.endswith('.hkp'): - path += '.hkp' - if name == None and path != None: - name = os.path.basename(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 @@ -99,11 +175,31 @@ class FilePlaylist (Playlist): >>> 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" + '\\\x14\x87\x88*q\xf8\xaa\xa7\x84f\x82\xa1S>\xfd3+\xd0o' """ string = self.flatten() return hashlib.sha1(string).digest() + def _clean_key(self, key): + """Replace spaces in keys with \\u00B7 (middle dot). + + This character is deemed unlikely to occur in keys to our + playlist and curve info dictionaries, while many keys have + spaces in them. + + \\u00B7 is allowed in XML 1.0 as of the 5th edition. See + the `4th edition errata`_ for details. + + .. _4th edition errata: + http://www.w3.org/XML/xml-V10-4e-errata#E09 + """ + return key.replace(' ', u'\u00B7') + + def _restore_key(self, key): + """Restore keys encoded with :meth:`_clean_key`. + """ + return key.replace(u'\u00B7', ' ') + def flatten(self, absolute_paths=False): """Create a string representation of the playlist. @@ -129,22 +225,23 @@ class FilePlaylist (Playlist): >>> 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' + >>> c.info['attr with spaces'] = 'The second curve\\nwith endlines' >>> p.append(c) - >>> print p.flatten() # doctest: +NORMALIZE_WHITESPACE +REPORT_UDIFF + >>> def _print(string): + ... escaped_string = unicode(string, 'utf-8').encode('unicode escape') + ... print escaped_string.replace('\\\\n', '\\n').replace('\\\\t', '\\t'), + >>> _print(p.flatten()) # doctest: +NORMALIZE_WHITESPACE +REPORT_UDIFF - - + + - - >>> print p.flatten(absolute_paths=True) # 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 @@ -153,7 +250,10 @@ class FilePlaylist (Playlist): 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)) + if (key in self._ignored_keys + or not isinstance(value, types.StringTypes)): + continue + root.setAttribute(self._clean_key(key), str(value)) for curve in self: # save curves and their attributes curve_element = doc.createElement('curve') root.appendChild(curve_element) @@ -161,20 +261,26 @@ class FilePlaylist (Playlist): if absolute_paths == False: path = os.path.relpath( path, - os.path.abspath(os.path.expanduser(self.path))) + os.path.dirname( + 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)) + if (key in self._ignored_keys + or not isinstance(value, types.StringTypes)): + continue + curve_element.setAttribute(self._clean_key(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): + def _from_xml_doc(self, doc, identify=True): """Load a playlist from an :class:`xml.dom.minidom.Document` instance. """ root = doc.documentElement for attribute,value in root.attributes.items(): + attribute = self._restore_key(attribute) if attribute == 'version': assert value == self.version, \ 'Cannot read v%s playlist with a v%s reader' \ @@ -185,13 +291,14 @@ class FilePlaylist (Playlist): self.info[attribute] = value for curve_element in doc.getElementsByTagName('curve'): path = curve_element.getAttribute('path') - info = dict(curve_element.attributes.items()) + info = dict([(self._restore_key(key), value) + for key,value in curve_element.attributes.items()]) info.pop('path') - self.append_curve_by_path(path, info, identify=False) + self.append_curve_by_path(path, info, identify=identify) self.jump(self._index) # ensure valid index - def from_string(self, string): - """Load a playlist from a string. + def from_string(self, string, identify=True): + u"""Load a playlist from a string. Examples -------- @@ -199,12 +306,12 @@ class FilePlaylist (Playlist): >>> string = ''' ... ... - ... + ... ... ... ''' >>> p = FilePlaylist(drivers=[], - ... path=os.path.join('path', 'to','playlist')) - >>> p.from_string(string) + ... path=os.path.join('path', 'to', 'my', 'playlist')) + >>> p.from_string(string, identify=False) >>> p._index 1 >>> p.info @@ -213,16 +320,19 @@ class FilePlaylist (Playlist): ... print curve.path path/to/curve/one path/to/curve/two + >>> p[-1].info['attr with spaces'] + u'The second curve\\nwith endlines' """ doc = xml.dom.minidom.parseString(string) - return self._from_xml_doc(doc) + self._from_xml_doc(doc, identify=identify) - def load(self, path=None): + def load(self, path=None, identify=True): """Load a playlist from a file. """ self.set_path(path) doc = xml.dom.minidom.parse(self.path) - return self._from_xml_doc(doc) + self._from_xml_doc(doc, identify=identify) + self._digest = self.digest() def save(self, path=None): """Saves the playlist in a XML file.