X-Git-Url: http://git.tremily.us/?a=blobdiff_plain;f=hooke%2Fplaylist.py;h=8444c9aac883f151e274a9810e7ef7ee4976326f;hb=9bad7f372d41deb4ad12a4ed692f15835fb49e84;hp=b073387bf9a7eb5336ba585d3c629eb62747cc89;hpb=5c9cff3fdd84cc3e496bce796425556bc4b2be00;p=hooke.git diff --git a/hooke/playlist.py b/hooke/playlist.py index b073387..8444c9a 100644 --- a/hooke/playlist.py +++ b/hooke/playlist.py @@ -1,3 +1,21 @@ +# 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,9 +24,11 @@ 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 NoteIndexList (list): @@ -24,10 +44,28 @@ class NoteIndexList (list): self.info = {} self._index = 0 + def __str__(self): + return '<%s %s>' % (self.__class__.__name__, self.name) + + def _setup_item(self, item): + """Perform any required initialization before returning an item. + """ + pass + + 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 - return self[self._index] + item = self[self._index] + self._setup_item(item) + return item def jump(self, index): if len(self) == 0: @@ -41,10 +79,21 @@ class NoteIndexList (list): def previous(self): self.jump(self._index - 1) - def filter(self, keeper_fn=lambda item:True): + 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 reversed(c): - if keeper_fn(item) != True: + 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()) @@ -60,10 +109,12 @@ class Playlist (NoteIndexList): 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(self.path, path) + path = os.path.join(os.path.dirname(self.path), path) path = os.path.normpath(path) c = curve.Curve(path, info=info) if identify == True: @@ -71,6 +122,19 @@ class Playlist (NoteIndexList): 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' @@ -78,6 +142,9 @@ 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 path != None: @@ -85,7 +152,7 @@ class FilePlaylist (Playlist): path += '.hkp' self.path = path if self.name == None: - name = os.path.basename(path) + self.name = os.path.basename(path) def is_saved(self): return self.digest() == self._digest @@ -108,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. @@ -138,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 @@ -162,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) @@ -170,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' \ @@ -194,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 -------- @@ -208,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 @@ -222,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.