X-Git-Url: http://git.tremily.us/?p=hooke.git;a=blobdiff_plain;f=hooke%2Fplaylist.py;h=79f785c5305b35415af5651da7e4f2d25ae0b86c;hp=473e7362c7b4e722ece4a4788d987e22fd764e44;hb=39dffab5d36a997cc76ad9069fe00de51d3147a0;hpb=300551c499d4f608433001477fe87936fbe8a4ba diff --git a/hooke/playlist.py b/hooke/playlist.py index 473e736..79f785c 100644 --- a/hooke/playlist.py +++ b/hooke/playlist.py @@ -30,7 +30,8 @@ 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 @@ -43,10 +44,8 @@ class NoteIndexList (list): """ def __init__(self, name=None): super(NoteIndexList, self).__init__() - self.name = name - self.info = {} - self._index = 0 - self._set_ignored_attrs() + self._set_default_attrs() + self.__setstate__({'name': name}) def __str__(self): return str(self.__unicode__()) @@ -57,56 +56,27 @@ class NoteIndexList (list): def __repr__(self): return self.__str__() - def _set_ignored_attrs(self): - self._ignored_attrs = ['_ignored_attrs', '_default_attrs'] + def _set_default_attrs(self): self._default_attrs = { 'info': {}, + 'name': None, + '_index': 0, } def __getstate__(self): - state = dict(self.__dict__) - for key in self._ignored_attrs: - if key in state: - del(state[key]) - for key,value in self._default_attrs.items(): - if key in state and state[key] == value: - del(state[key]) - assert 'items' not in state - state['items'] = [] - self._assert_clean_state(self, state) - for item in self: # save curves and their attributes - item_state = self._item_getstate(item) - self._assert_clean_state(item, item_state) - state['items'].append(item_state) - return state + return self.__dict__.copy() def __setstate__(self, state): - self._set_ignored_attrs() - for key,value in self._default_attrs.items(): - setattr(self, key, value) - for key,value in state.items(): - if key == 'items': - continue - setattr(self, key, value) - for item_state in state['items']: - self.append(self._item_setstate(item_state)) - - def _item_getstate(self, item): - return item - - def _item_setstate(self, state): - return state - - def _assert_clean_state(self, owner, state): - for k,v in state.items(): - if k == 'drivers': # HACK. Need better driver serialization. - continue - try: - yaml.safe_dump((k,v)) - except RepresenterError, e: - raise NotImplementedError( - 'cannot convert %s.%s = %s (%s) to safe YAML' - % (owner.__class__.__name__, k, v, type(v))) + 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. @@ -121,11 +91,12 @@ class NoteIndexList (list): 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 item = self[self._index] - self._setup_item(item) + if load == True: + self._setup_item(item) return item def jump(self, index): @@ -183,52 +154,34 @@ class NoteIndexList (list): 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. """ def __init__(self, drivers, name=None): super(Playlist, self).__init__(name=name) self.drivers = drivers - self._max_loaded = 100 # curves to hold in memory simultaneously. - - def _set_ignored_attrs(self): - super(Playlist, self)._set_ignored_attrs() - self._ignored_attrs.extend([ - '_item_ignored_attrs', '_item_default_attrs', - '_loaded']) - self._item_ignored_attrs = [] - self._item_default_attrs = { - 'command_stack': [], - 'data': None, - 'driver': None, - 'info': {}, - 'name': None, - } - self._loaded = [] # List of loaded curves, see :meth:`._setup_item`. - - def _item_getstate(self, item): - assert isinstance(item, curve.Curve), type(item) - state = item.__getstate__() - for key in self._item_ignored_attrs: - if key in state: - del(state[key]) - for key,value in self._item_default_attrs.items(): - if key in state and state[key] == value: - del(state[key]) - return state - def _item_setstate(self, state): - for key,value in self._item_default_attrs.items(): - if key not in state: - state[key] = value - item = curve.Curve(path=None) - item.__setstate__(state) - return item + 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) @@ -248,9 +201,67 @@ class Playlist (NoteIndexList): 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): """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' @@ -258,12 +269,14 @@ class FilePlaylist (Playlist): super(FilePlaylist, self).__init__(drivers, name) self.path = self._base_path = None self.set_path(path) - self._relative_curve_paths = True + self.relative_curve_paths = True + self._relative_curve_paths = False - def _set_ignored_attrs(self): - super(FilePlaylist, self)._set_ignored_attrs() - self._ignored_attrs.append('_digest') - self._digest = None + 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__() @@ -272,45 +285,47 @@ class FilePlaylist (Playlist): return state def __setstate__(self, state): - assert('version') in state, state - version = state.pop('version') - assert version == FilePlaylist.version, ( - 'invalid version %s (%s) != %s (%s)' - % (version, type(version), - FilePlaylist.version, type(FilePlaylist.version))) + 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 _item_getstate(self, item): - state = super(FilePlaylist, self)._item_getstate(item) - if state.get('path', None) != None: - path = os.path.abspath(os.path.expanduser(state['path'])) - if self._relative_curve_paths == True: - path = os.path.relpath(path, self._base_path) - state['path'] = path - return state - - def _item_setstate(self, state): - item = super(FilePlaylist, self)._item_setstate(state) - if 'path' in state: - item.set_path(os.path.join(self._base_path, state['path'])) - return item - def set_path(self, path): + orig_base_path = getattr(self, '_base_path', None) if path == None: if self._base_path == None: self._base_path = os.getcwd() else: - if not path.endswith('.hkp'): - path += '.hkp' + 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: 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): - if self._base_path != None: - path = os.path.join(self._base_path, path) + path = self._curve_path(path) super(FilePlaylist, self).append_curve_by_path(path, *args, **kwargs) def is_saved(self): @@ -327,14 +342,14 @@ 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\x1ax\xb1|\x84uA\xe4\x1d\xbf`\x004|\x82\xc2\xdd\xc1\x9e' + 'f\xe26i\xb98i\x1f\xb61J7:\xf2\x8e\x1d\xde\xc3}g' """ string = self.flatten() return hashlib.sha1(string).digest() @@ -344,10 +359,14 @@ class FilePlaylist (Playlist): A playlist is a YAML document with the following minimal syntax:: - version: '0.2' - items: - - path: picoforce.000 - - path: picoforce.001 + !!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. @@ -361,142 +380,89 @@ 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['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, 'arg 1':'Y'}), + ... CommandMessage('command B', {'arg 0':1, 'curve':c}), ... ]) - >>> p.append(c) + >>> p.append_curve(c) >>> print p.flatten() # doctest: +REPORT_UDIFF # Hooke playlist version 0.2 - _base_path: /path/to - _index: 0 - _max_loaded: 100 - _relative_curve_paths: true - drivers: [] - info: {note: An example playlist} - items: - - info: {note: The first curve} + !!python/object/new:hooke.playlist.FilePlaylist + listitems: + - !!python/object:hooke.curve.Curve + info: {note: The first curve} name: one path: curve/one - - command_stack: - - arguments: {arg 0: 0, arg 1: X} - command: command A - - arguments: {arg 0: 1, arg 1: Y} - command: command B + - &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: curve/two - name: playlist.hkp - path: /path/to/playlist.hkp - version: '0.2' + 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 + >>> p.relative_curve_paths = False >>> print p.flatten() # doctest: +REPORT_UDIFF # Hooke playlist version 0.2 - _base_path: /path/to - _index: 0 - _max_loaded: 100 - _relative_curve_paths: false - drivers: [] - info: {note: An example playlist} - items: - - info: {note: The first curve} + !!python/object/new:hooke.playlist.FilePlaylist + listitems: + - !!python/object:hooke.curve.Curve + info: {note: The first curve} name: one path: /path/to/curve/one - - command_stack: - - arguments: {arg 0: 0, arg 1: X} - command: command A - - arguments: {arg 0: 1, arg 1: Y} - command: command B + - &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 - name: playlist.hkp - path: /path/to/playlist.hkp - version: '0.2' + 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' """ - yaml_string = yaml.dump(self.__getstate__(), allow_unicode=True) - return ('# Hooke playlist version %s\n' % self.version) + yaml_string - - def from_string(self, string): - u"""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 - -------- - - Minimal example. - - >>> string = '''# Hooke playlist version 0.2 - ... version: '0.2' - ... items: - ... - path: picoforce.000 - ... - path: picoforce.001 - ... ''' - >>> p = FilePlaylist(drivers=[], - ... path=os.path.join('/path', 'to', 'my', 'playlist')) - >>> p.from_string(string) - >>> for curve in p: - ... print curve.path - /path/to/my/picoforce.000 - /path/to/my/picoforce.001 - - More complicated example. - - >>> string = '''# Hooke playlist version 0.2 - ... _base_path: /path/to - ... _digest: null - ... _index: 1 - ... _max_loaded: 100 - ... _relative_curve_paths: true - ... info: {note: An example playlist} - ... items: - ... - info: {note: The first curve} - ... path: curve/one - ... - command_stack: - ... - arguments: {arg 0: 0, arg 1: X} - ... command: command A - ... - arguments: {arg 0: 1, arg 1: Y} - ... command: command B - ... info: {attr with spaces: 'The second curve - ... - ... with endlines'} - ... name: two - ... path: curve/two - ... name: playlist.hkp - ... path: /path/to/playlist.hkp - ... version: '0.2' - ... ''' - >>> p = FilePlaylist(drivers=[], - ... path=os.path.join('path', 'to', 'my', 'playlist')) - >>> p.from_string(string) - >>> 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[-1].command_stack # doctest: +NORMALIZE_WHITESPACE - [, - ] - """ - state = yaml.load(string) - self.__setstate__(state) + yaml_string = yaml.dump(self, allow_unicode=True) + self.update_curve_paths() + return ('# Hooke playlist version %s\n' % self.version) + yaml_string def save(self, path=None, makedirs=True): """Saves the playlist to a YAML file. @@ -509,31 +475,114 @@ class FilePlaylist (Playlist): f.write(self.flatten()) self._digest = self.digest() - def load(self, path=None, identify=True, hooke=None): - """Load a playlist from a file. - """ - self.set_path(path) - with open(self.path, 'r') as f: - text = f.read() - self.from_string(text) - self._digest = self.digest() - for curve in self: - curve.set_hooke(hooke) - if identify == True: - curve.identify(self.drivers) +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) -class Playlists (NoteIndexList): - """A :class:`NoteIndexList` of :class:`FilePlaylist`\s. +def load(path=None, drivers=None, identify=True, hooke=None): + """Load a playlist from a file. """ - def __init__(self, *arg, **kwargs): - super(Playlists, self).__init__(*arg, **kwargs) + 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 - def _item_getstate(self, item): - assert isinstance(item, FilePlaylist), type(item) - return item.__getstate__() - def _item_setstate(self, state): - item = FilePlaylist(drivers=[]) - item.__setstate__(state) - return item +class Playlists (NoteIndexList): + """A :class:`NoteIndexList` of :class:`FilePlaylist`\s. + """ + pass