X-Git-Url: http://git.tremily.us/?p=hooke.git;a=blobdiff_plain;f=hooke%2Fplaylist.py;h=79f785c5305b35415af5651da7e4f2d25ae0b86c;hp=b073387bf9a7eb5336ba585d3c629eb62747cc89;hb=39dffab5d36a997cc76ad9069fe00de51d3147a0;hpb=5c9cff3fdd84cc3e496bce796425556bc4b2be00 diff --git a/hooke/playlist.py b/hooke/playlist.py index b073387..79f785c 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. @@ -5,10 +23,16 @@ import copy import hashlib +import os import os.path -import xml.dom.minidom +import types + +import yaml +from yaml.representer import RepresenterError -from . import curve as curve +from .command_stack import CommandStack +from .curve import Curve +from .util.itertools import reverse_enumerate class NoteIndexList (list): @@ -20,14 +44,60 @@ class NoteIndexList (list): """ def __init__(self, name=None): super(NoteIndexList, self).__init__() - self.name = name - self.info = {} - self._index = 0 + self._set_default_attrs() + self.__setstate__({'name': name}) + + def __str__(self): + return str(self.__unicode__()) + + def __unicode__(self): + return u'<%s %s>' % (self.__class__.__name__, self.name) + + def __repr__(self): + return self.__str__() + + def _set_default_attrs(self): + self._default_attrs = { + 'info': {}, + 'name': None, + '_index': 0, + } + + def __getstate__(self): + return self.__dict__.copy() + + def __setstate__(self, state): + self._set_default_attrs() + if state == True: + return + self.__dict__.update(self._default_attrs) + try: + self.__dict__.update(state) + except TypeError, e: + print state, type(state), e + if self.info in [None, {}]: + self.info = {} + + 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): + def current(self, load=True): if len(self) == 0: return None - return self[self._index] + item = self[self._index] + if load == True: + self._setup_item(item) + return item def jump(self, index): if len(self) == 0: @@ -41,10 +111,40 @@ 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. + + 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.ApplyCommand`. After the + iteration completes, :attr:`_index` is restored to its + original value. + """ + index = self._index + items = self + if reverse == True: + items = reverse_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, load_curves=True, + *args, **kwargs): c = copy.deepcopy(self) - for item in reversed(c): - if keeper_fn(item) != True: + if load_curves == True: + items = c.items(reverse=True) + else: + items = reversed(c) + for item in items: + if keeper_fn(item, *args, **kwargs) != True: c.remove(item) try: # attempt to maintain the same current item c._index = c.index(self.current()) @@ -52,8 +152,9 @@ class NoteIndexList (list): c._index = 0 return c + class Playlist (NoteIndexList): - """A :class:`NoteIndexList` of :class:`hooke.curve.Curve`\s. + """A :class:`NoteIndexList` of :class:`hooke.Curve`\s. Keeps a list of :attr:`drivers` for loading curves. """ @@ -61,31 +162,171 @@ class Playlist (NoteIndexList): 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) + def _set_default_attrs(self): + super(Playlist, self)._set_default_attrs() + self._default_attrs['drivers'] = [] + # List of loaded curves, see :meth:`._setup_item`. + self._default_attrs['_loaded'] = [] + self._default_attrs['_max_loaded'] = 100 # curves to hold in memory simultaneously. + + def __setstate__(self, state): + super(Playlist, self).__setstate__(state) + if self.drivers in [None, {}]: + self.drivers = [] + if self._loaded in [None, {}]: + self._loaded = [] + + def append_curve(self, curve): + self.append(curve) + + 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 = Curve(path, info=info) + c.set_hooke(hooke) 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() + + def unload(self, curve): + "Inverse of .`_setup_item`." + curve.unload() + try: + self._loaded.remove(curve) + except ValueError: + pass + + +def playlist_path(path): + """Normalize playlist path extensions. + + Examples + -------- + >>> print playlist_path('playlist') + playlist.hkp + >>> print playlist_path('playlist.hkp') + playlist.hkp + >>> print playlist_path(None) + None + """ + if path == None: + return None + if not path.endswith('.hkp'): + path += '.hkp' + return path + + class FilePlaylist (Playlist): - version = '0.1' + """A file-backed :class:`Playlist`. + + Examples + -------- + + >>> p = FilePlaylist(drivers=['Driver A', 'Driver B']) + >>> p.append(Curve('dummy/path/A')) + >>> p.append(Curve('dummy/path/B')) + + The data-type is pickleable, to ensure we can move it between + processes with :class:`multiprocessing.Queue`\s. + + >>> import pickle + >>> s = pickle.dumps(p) + >>> z = pickle.loads(s) + >>> for curve in z: + ... print curve + + + >>> print z.drivers + ['Driver A', 'Driver B'] + + The data-type is also YAMLable (see :mod:`hooke.util.yaml`). + + >>> s = yaml.dump(p) + >>> z = yaml.load(s) + >>> for curve in z: + ... print curve + + + >>> print z.drivers + ['Driver A', 'Driver B'] + """ + version = '0.2' def __init__(self, drivers, name=None, path=None): super(FilePlaylist, self).__init__(drivers, name) + self.path = self._base_path = None self.set_path(path) - self._digest = None + self.relative_curve_paths = True + self._relative_curve_paths = False + + def _set_default_attrs(self): + super(FilePlaylist, self)._set_default_attrs() + self._default_attrs['relative_curve_paths'] = True + self._default_attrs['_relative_curve_paths'] = False + self._default_attrs['_digest'] = None + + def __getstate__(self): + state = super(FilePlaylist, self).__getstate__() + assert 'version' not in state, state + state['version'] = self.version + return state + + def __setstate__(self, state): + if 'version' in state: + version = state.pop('version') + assert version == FilePlaylist.version, ( + 'invalid version %s (%s) != %s (%s)' + % (version, type(version), + FilePlaylist.version, type(FilePlaylist.version))) + super(FilePlaylist, self).__setstate__(state) def set_path(self, path): - if path != None: - if not path.endswith('.hkp'): - path += '.hkp' + orig_base_path = getattr(self, '_base_path', None) + if path == None: + if self._base_path == None: + self._base_path = os.getcwd() + else: + path = playlist_path(path) self.path = path + self._base_path = os.path.dirname(os.path.abspath( + os.path.expanduser(self.path))) if self.name == None: - name = os.path.basename(path) + self.name = os.path.basename(path) + if self._base_path != orig_base_path: + self.update_curve_paths() + + def update_curve_paths(self): + for curve in self: + curve.set_path(self._curve_path(curve.path)) + + def _curve_path(self, path): + if self._base_path == None: + self._base_path = os.getcwd() + path = os.path.join(self._base_path, path) + if self._relative_curve_paths == True: + path = os.path.relpath(path, self._base_path) + return path + + def append_curve(self, curve): + curve.set_path(self._curve_path(curve.path)) + super(FilePlaylist, self).append_curve(curve) + + def append_curve_by_path(self, path, *args, **kwargs): + path = self._curve_path(path) + super(FilePlaylist, self).append_curve_by_path(path, *args, **kwargs) def is_saved(self): return self.digest() == self._digest @@ -101,142 +342,247 @@ class FilePlaylist (Playlist): >>> 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 = 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')) + >>> p.append_curve(c) + >>> c = Curve(os.path.join(root_path, 'to', 'curve', 'two')) >>> c.info['note'] = 'The second curve' - >>> p.append(c) + >>> p.append_curve(c) >>> p.digest() - "\xa1\x99\x8a\x99\xed\xad\x13'\xa7w\x12\x00\x07Z\xb3\xd0zN\xa2\xe1" + 'f\xe26i\xb98i\x1f\xb61J7:\xf2\x8e\x1d\xde\xc3}g' """ string = self.flatten() return hashlib.sha1(string).digest() - def flatten(self, absolute_paths=False): + def flatten(self): """Create a string representation of the playlist. - A playlist is an XML document with the following syntax:: + A playlist is a YAML document with the following minimal syntax:: - - - - - + !!python/object/new:hooke.playlist.FilePlaylist + state: + version: '0.2' + listitems: + - !!python/object:hooke.curve.Curve + path: /path/to/curve/one + - !!python/object:hooke.curve.Curve + path: /path/to/curve/two Relative paths are interpreted relative to the location of the playlist file. - + Examples -------- + >>> from .engine import CommandMessage + >>> 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 = 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 - - - - - + >>> p.append_curve(c) + >>> c = Curve(os.path.join(root_path, 'to', 'curve', 'two')) + >>> c.info['attr with spaces'] = 'The second curve\\nwith endlines' + >>> c.command_stack.extend([ + ... CommandMessage('command A', {'arg 0':0, 'arg 1':'X'}), + ... CommandMessage('command B', {'arg 0':1, 'curve':c}), + ... ]) + >>> p.append_curve(c) + >>> print p.flatten() # doctest: +REPORT_UDIFF + # Hooke playlist version 0.2 + !!python/object/new:hooke.playlist.FilePlaylist + listitems: + - !!python/object:hooke.curve.Curve + info: {note: The first curve} + name: one + path: curve/one + - &id001 !!python/object:hooke.curve.Curve + command_stack: !!python/object/new:hooke.command_stack.CommandStack + listitems: + - !!python/object:hooke.engine.CommandMessage + arguments: {arg 0: 0, arg 1: X} + command: command A + - !!python/object:hooke.engine.CommandMessage + arguments: + arg 0: 1 + curve: *id001 + command: command B + info: {attr with spaces: 'The second curve - >>> print p.flatten(absolute_paths=True) # doctest: +NORMALIZE_WHITESPACE +REPORT_UDIFF - - - - - + with endlines'} + name: two + path: curve/two + state: + _base_path: /path/to + info: {note: An example playlist} + name: playlist.hkp + path: /path/to/playlist.hkp + version: '0.2' + + >>> p.relative_curve_paths = False + >>> print p.flatten() # doctest: +REPORT_UDIFF + # Hooke playlist version 0.2 + !!python/object/new:hooke.playlist.FilePlaylist + listitems: + - !!python/object:hooke.curve.Curve + info: {note: The first curve} + name: one + path: /path/to/curve/one + - &id001 !!python/object:hooke.curve.Curve + command_stack: !!python/object/new:hooke.command_stack.CommandStack + listitems: + - !!python/object:hooke.engine.CommandMessage + arguments: {arg 0: 0, arg 1: X} + command: command A + - !!python/object:hooke.engine.CommandMessage + arguments: + arg 0: 1 + curve: *id001 + command: command B + info: {attr with spaces: 'The second curve + + with endlines'} + name: two + path: /path/to/curve/two + state: + _base_path: /path/to + info: {note: An example playlist} + name: playlist.hkp + path: /path/to/playlist.hkp + relative_curve_paths: false + version: '0.2' """ - 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. + rcp = self._relative_curve_paths + self._relative_curve_paths = self.relative_curve_paths + self.update_curve_paths() + self._relative_curve_paths = rcp - Examples - -------- + yaml_string = yaml.dump(self, allow_unicode=True) + self.update_curve_paths() + return ('# Hooke playlist version %s\n' % self.version) + yaml_string - >>> 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. + def save(self, path=None, makedirs=True): + """Saves the playlist to a YAML file. """ self.set_path(path) - doc = xml.dom.minidom.parse(self.path) - return self._from_xml_doc(doc) + dirname = os.path.dirname(self.path) or '.' + if makedirs == True and not os.path.isdir(dirname): + os.makedirs(dirname) + with open(self.path, 'w') as f: + f.write(self.flatten()) + self._digest = self.digest() - 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() + +def from_string(string): + u"""Load a playlist from a string. + + Examples + -------- + + Minimal example. + + >>> string = '''# Hooke playlist version 0.2 + ... !!python/object/new:hooke.playlist.FilePlaylist + ... state: + ... version: '0.2' + ... listitems: + ... - !!python/object:hooke.curve.Curve + ... path: curve/one + ... - !!python/object:hooke.curve.Curve + ... path: curve/two + ... ''' + >>> p = from_string(string) + >>> p.set_path('/path/to/playlist') + >>> for curve in p: + ... print curve.name, curve.path + one /path/to/curve/one + two /path/to/curve/two + + More complicated example. + + >>> string = '''# Hooke playlist version 0.2 + ... !!python/object/new:hooke.playlist.FilePlaylist + ... listitems: + ... - !!python/object:hooke.curve.Curve + ... info: {note: The first curve} + ... name: one + ... path: /path/to/curve/one + ... - &id001 !!python/object:hooke.curve.Curve + ... command_stack: !!python/object/new:hooke.command_stack.CommandStack + ... listitems: + ... - !!python/object:hooke.engine.CommandMessage + ... arguments: {arg 0: 0, arg 1: X} + ... command: command A + ... - !!python/object:hooke.engine.CommandMessage + ... arguments: + ... arg 0: 1 + ... curve: *id001 + ... command: command B + ... info: {attr with spaces: 'The second curve + ... + ... with endlines'} + ... name: two + ... path: /path/to/curve/two + ... state: + ... _base_path: /path/to + ... _index: 1 + ... info: {note: An example playlist} + ... name: playlist.hkp + ... path: /path/to/playlist.hkp + ... version: '0.2' + ... ''' + >>> p = from_string(string) + >>> p.set_path('/path/to/playlist') + >>> p._index + 1 + >>> p.info + {'note': 'An example playlist'} + >>> for curve in p: + ... print curve.name, curve.path + one /path/to/curve/one + two /path/to/curve/two + >>> p[-1].info['attr with spaces'] + 'The second curve\\nwith endlines' + >>> type(p[-1].command_stack) + + >>> p[0].command_stack + [] + >>> type(p[0].command_stack) + + >>> p[-1].command_stack # doctest: +NORMALIZE_WHITESPACE + [, + }>] + >>> type(p[1].command_stack) + + >>> c2 = p[-1] + >>> c2.command_stack[-1].arguments['curve'] == c2 + True + """ + return yaml.load(string) + +def load(path=None, drivers=None, identify=True, hooke=None): + """Load a playlist from a file. + """ + path = playlist_path(path) + with open(path, 'r') as f: + text = f.read() + playlist = from_string(text) + playlist.set_path(path) + playlist._digest = playlist.digest() + if drivers != None: + playlist.drivers = drivers + playlist.set_path(path) + for curve in playlist: + curve.set_hooke(hooke) + if identify == True: + curve.identify(playlist.drivers) + return playlist + + +class Playlists (NoteIndexList): + """A :class:`NoteIndexList` of :class:`FilePlaylist`\s. + """ + pass