X-Git-Url: http://git.tremily.us/?p=hooke.git;a=blobdiff_plain;f=hooke%2Fplaylist.py;h=dfec6d72c28c65b3888f6b4d5265b43fee523d73;hp=59daa462c4525c1e7f209a75cfbed53c3d462629;hb=b90995fb4b6d8151df862d40edc8c369d7052cfa;hpb=cd768c9704dcb751b43e019bb137924fbd5b8e15 diff --git a/hooke/playlist.py b/hooke/playlist.py index 59daa46..dfec6d7 100644 --- a/hooke/playlist.py +++ b/hooke/playlist.py @@ -2,15 +2,15 @@ # # 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 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. +# 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 @@ -24,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): @@ -43,13 +45,27 @@ class NoteIndexList (list): self._index = 0 def __str__(self): - return '<%s %s>' % (self.__class__.__name__, self.name) + return str(self.__unicode__()) + + def __unicode__(self): + return u'<%s %s>' % (self.__class__.__name__, self.name) + + def __repr__(self): + return self.__str__() 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 @@ -69,10 +85,34 @@ class NoteIndexList (list): def previous(self): self.jump(self._index - 1) + def items(self, reverse=False): + """Iterate through `self` calling `_setup_item` on each item + before yielding. + + Notes + ----- + Updates :attr:`_index` during the iteration so + :func:`~hooke.plugin.curve.current_curve_callback` works as + expected in :class:`~hooke.command.Command`\s called from + :class:`~hooke.plugin.playlist.ApplyCommandStack`. After the + iteration completes, :attr:`_index` is restored to its + original value. + """ + index = self._index + items = self + if reverse == True: + items = reversed(enumerate(self)) + else: + items = enumerate(self) + for i,item in items: + self._index = i + self._setup_item(item) + yield item + self._index = index + def filter(self, keeper_fn=lambda item:True, *args, **kwargs): c = copy.deepcopy(self) - for item in reversed(c): - c._setup_item(item) + 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 @@ -81,6 +121,7 @@ class NoteIndexList (list): c._index = 0 return c + class Playlist (NoteIndexList): """A :class:`NoteIndexList` of :class:`hooke.curve.Curve`\s. @@ -92,11 +133,10 @@ class Playlist (NoteIndexList): 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) + def append_curve_by_path(self, path, info=None, identify=True, hooke=None): path = os.path.normpath(path) c = curve.Curve(path, info=info) + c.set_hooke(hooke) if identify == True: c.identify(self.drivers) self.append(c) @@ -115,13 +155,18 @@ class Playlist (NoteIndexList): oldest = self._loaded.pop(0) oldest.unload() + class FilePlaylist (Playlist): version = '0.1' def __init__(self, drivers, name=None, path=None): super(FilePlaylist, self).__init__(drivers, name) + self.path = None self.set_path(path) self._digest = None + self._ignored_keys = [ + 'experiment', # class instance, not very exciting. + ] def set_path(self, path): if path != None: @@ -131,6 +176,11 @@ class FilePlaylist (Playlist): if self.name == None: self.name = os.path.basename(path) + def append_curve_by_path(self, path, *args, **kwargs): + if self.path != None: + path = os.path.join(os.path.dirname(self.path), path) + super(FilePlaylist, self).append_curve_by_path(path, *args, **kwargs) + def is_saved(self): return self.digest() == self._digest @@ -157,6 +207,26 @@ class FilePlaylist (Playlist): 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. @@ -182,22 +252,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 @@ -206,7 +277,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) @@ -219,7 +293,10 @@ class FilePlaylist (Playlist): 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 @@ -230,6 +307,7 @@ class FilePlaylist (Playlist): """ 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' \ @@ -240,13 +318,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=identify) self.jump(self._index) # ensure valid index def from_string(self, string, identify=True): - """Load a playlist from a string. + u"""Load a playlist from a string. Examples -------- @@ -254,7 +333,7 @@ class FilePlaylist (Playlist): >>> string = ''' ... ... - ... + ... ... ... ''' >>> p = FilePlaylist(drivers=[], @@ -268,22 +347,26 @@ 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) self._from_xml_doc(doc, identify=identify) - def load(self, path=None, identify=True): + def load(self, path=None, identify=True, hooke=None): """Load a playlist from a file. """ self.set_path(path) doc = xml.dom.minidom.parse(self.path) self._from_xml_doc(doc, identify=identify) self._digest = self.digest() - + for curve in self: + curve.set_hooke(hooke) + 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() + with open(self.path, 'w') as f: + f.write(self.flatten()) + self._digest = self.digest()