X-Git-Url: http://git.tremily.us/?p=hooke.git;a=blobdiff_plain;f=hooke%2Fplugin%2Fplaylist.py;h=80c22d2ffa3628ebba2115ece97123d7f52dec78;hp=58832d3537e39891b28a9c8981e62fc6aada887d;hb=7c52f54a7fc8654bc6783ff815c93166b917faf5;hpb=5ba82c34a522c41e0ef93b61cafa6f608a94d651 diff --git a/hooke/plugin/playlist.py b/hooke/plugin/playlist.py index 58832d3..80c22d2 100644 --- a/hooke/plugin/playlist.py +++ b/hooke/plugin/playlist.py @@ -1,325 +1,234 @@ -"""Defines :class:`PlaylistPlugin` several associated -:class:`hooke.plugin.Command`\s. +# 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 :class:`PlaylistPlugin` and +several associated :class:`hooke.command.Command`\s for handling +:mod:`hooke.playlist` classes. """ -import copy -import hashlib -import os +import glob import os.path -import xml.dom.minidom +from ..command import Command, Argument, Failure +from ..playlist import FilePlaylist +from . import Builtin -from .. import curve as curve -from ..plugin import Plugin, Command, Argument -class PlaylistPlugin (Plugin): +class PlaylistPlugin (Builtin): def __init__(self): super(PlaylistPlugin, self).__init__(name='playlist') + self._commands = [ + NextCommand(self), PreviousCommand(self), JumpCommand(self), + GetCommand(self), IndexCommand(self), CurveListCommand(self), + SaveCommand(self), LoadCommand(self), + AddCommand(self), AddGlobCommand(self), + RemoveCommand(self), ApplyCommandStack(self), + FilterCommand(self), NoteFilterCommand(self), + ] + + +# Define common or complicated arguments + +def current_playlist_callback(hooke, command, argument, value): + if value != None: + return value + playlist = hooke.playlists.current() + if playlist == None: + raise Failure('No playlists loaded') + return playlist + +PlaylistArgument = Argument( + name='playlist', type='playlist', callback=current_playlist_callback, + help=""" +:class:`hooke.playlist.Playlist` to act on. Defaults to the current +playlist. +""".strip()) + +def playlist_name_callback(hooke, command, argument, value): + if value != None: + return value + i = 0 + names = [p.name for p in hooke.playlists] + while True: + name = 'playlist-%d' % i + if name not in names: + return name + i += 1 + +PlaylistNameArgument = Argument( + name='output playlist', type='string', optional=True, + callback=playlist_name_callback, + help=""" +Name of the new playlist (defaults to an auto-generated name). +""".strip()) + +def all_drivers_callback(hooke, command, argument, value): + return hooke.drivers + + +# Define useful command subclasses + +class PlaylistCommand (Command): + """A :class:`~hooke.command.Command` operating on a + :class:`~hooke.playlist.Playlist`. + """ + def __init__(self, **kwargs): + if 'arguments' in kwargs: + kwargs['arguments'].insert(0, PlaylistArgument) + else: + kwargs['arguments'] = [PlaylistArgument] + super(PlaylistCommand, self).__init__(**kwargs) - def commands(self): - return [NextCommand(), PreviousCommand(), JumpCommand(), - SaveCommand(), LoadCommand(), - AddCommand(), RemoveCommand(), FilterCommand()] + def _playlist(self, hooke, params): + """Get the selected playlist. + + Notes + ----- + `hooke` is intended to attach the selected playlist to the + local hooke instance; the returned playlist should not be + effected by the state of `hooke`. + """ + # HACK? rely on params['playlist'] being bound to the local + # hooke (i.e. not a copy, as you would get by passing a + # playlist through the queue). Ugh. Stupid queues. As an + # alternative, we could pass lookup information through the + # queue... + return params['playlist'] -class Playlist (list): - """A list of :class:`hooke.curve.Curve`\s. - 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`). +class PlaylistAddingCommand (Command): + """A :class:`~hooke.command.Command` adding a + :class:`~hooke.playlist.Playlist`. """ - def __init__(self, drivers, name=None): - super(Playlist, self).__init__() - self.drivers = drivers - 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 active_curve(self): - return self[self._index] - - def has_curves(self): - return len(self) > 0 - - def jump(self, index): - if len(self) == 0: - self._index = 0 + def __init__(self, **kwargs): + if 'arguments' in kwargs: + kwargs['arguments'].insert(0, PlaylistNameArgument) else: - self._index = index % len(self) - - def next(self): - self.jump(self._index + 1) - - 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()) - except ValueError: - playlist._index = 0 - return playlist - -class FilePlaylist (Playlist): - version = '0.1' - - def __init__(self, drivers, name=None, path=None): - if name == None and path != None: - name = os.path.basename(path) - super(FilePlaylist, self).__init__(drivers, name) - self.path = path - self._digest = None - - def is_saved(self): - return self.digest() == self._digest - - def digest(self): - r"""Compute the sha1 digest of the flattened playlist - representation. - - Examples - -------- - - >>> 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.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) - >>> p.digest() - "\xa1\x99\x8a\x99\xed\xad\x13'\xa7w\x12\x00\x07Z\xb3\xd0zN\xa2\xe1" - """ - string = self.flatten() - return hashlib.sha1(string).digest() - - def flatten(self, absolute_paths=False): - """Create a string representation of the playlist. - - A playlist is an XML document with the following syntax:: - - - - - - - - Relative paths are interpreted relative to the location of the - playlist file. - - Examples - -------- - - >>> 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.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 - - - - - - - >>> 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 - 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. - - Examples - -------- - - >>> 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) + kwargs['arguments'] = [PlaylistNameArgument] + super(PlaylistAddingCommand, self).__init__(**kwargs) - def load(self, path=None): - """Load a playlist from a file. + def _set_playlist(self, hooke, params, playlist): + """Attach a new playlist. """ - if path != None: - self.path = path - if self.name == None: - self.name = os.path.basename(self.path) - doc = xml.dom.minidom.parse(path) - return self._from_xml_doc(doc) - - def save(self, path): - """Saves the playlist in a XML file. - """ - f = file(path, 'w') - f.write(self.flatten()) - f.close() + playlist.name = params['output playlist'] + hooke.playlists.append(playlist) + + +# Define commands -class NextCommand (Command): +class NextCommand (PlaylistCommand): """Move playlist to the next curve. """ - def __init__(self): + def __init__(self, plugin): super(NextCommand, self).__init__( - name='next curve', - arguments=[ - Argument(name='playlist', type='playlist', optional=False, - help=""" -:class:``hooke.plugin.playlist.Playlist`` to act on. -""".strip()), - ], - help=self.__doc__) + name='next curve', help=self.__doc__, plugin=plugin) + + def _run(self, hooke, inqueue, outqueue, params): + self._playlist(hooke, params).next() - def _run(inqueue, outqueue, params): - params['playlist'].next() -class PreviousCommand (Command): +class PreviousCommand (PlaylistCommand): """Move playlist to the previous curve. """ - def __init__(self): + def __init__(self, plugin): super(PreviousCommand, self).__init__( - name='previous curve', - arguments=[ - Argument(name='playlist', type='playlist', optional=False, - help=""" -:class:``hooke.plugin.playlist.Playlist`` to act on. -""".strip()), - ], - help=self.__doc__) + name='previous curve', help=self.__doc__, plugin=plugin) - def _run(inqueue, outqueue, params): - params['playlist'].previous() + def _run(self, hooke, inqueue, outqueue, params): + self._playlist(hooke, params).previous() -class JumpCommand (Command): + +class JumpCommand (PlaylistCommand): """Move playlist to a given curve. """ - def __init__(self): + def __init__(self, plugin): super(JumpCommand, self).__init__( name='jump to curve', arguments=[ - Argument(name='playlist', type='playlist', optional=False, - help=""" -:class:``hooke.plugin.playlist.Playlist`` to act on. -""".strip()), Argument(name='index', type='int', optional=False, help=""" Index of target curve. """.strip()), ], - help=self.__doc__) + help=self.__doc__, plugin=plugin) + + def _run(self, hooke, inqueue, outqueue, params): + self._playlist(hooke, params).jump(params['index']) + + +class IndexCommand (PlaylistCommand): + """Print the index of the current curve. + + The first curve has index 0. + """ + def __init__(self, plugin): + super(IndexCommand, self).__init__( + name='curve index', help=self.__doc__, plugin=plugin) + + def _run(self, hooke, inqueue, outqueue, params): + outqueue.put(self._playlist(hooke, params).index()) + - def _run(inqueue, outqueue, params): - params['playlist'].jump(params['index']) +class GetCommand (PlaylistCommand): + """Return a :class:`hooke.playlist.Playlist`. + """ + def __init__(self, plugin): + super(GetCommand, self).__init__( + name='get playlist', help=self.__doc__, plugin=plugin) + + def _run(self, hooke, inqueue, outqueue, params): + outqueue.put(self._playlist(hooke, params)) + + +class CurveListCommand (PlaylistCommand): + """Get the curves in a playlist. + """ + def __init__(self, plugin): + super(CurveListCommand, self).__init__( + name='playlist curves', help=self.__doc__, plugin=plugin) + + def _run(self, hooke, inqueue, outqueue, params): + outqueue.put(list(self._playlist(hooke, params))) -class SaveCommand (Command): + +class SaveCommand (PlaylistCommand): """Save a playlist. """ - def __init__(self): + def __init__(self, plugin): super(SaveCommand, self).__init__( name='save playlist', arguments=[ - Argument(name='playlist', type='playlist', optional=False, - help=""" -:class:``hooke.plugin.playlist.Playlist`` to act on. -""".strip()), Argument(name='output', type='file', help=""" -File name for the output playlist. Defaults to overwring the input -playlist. +File name for the output playlist. Defaults to overwriting the input +playlist. If the playlist does not have an input file (e.g. it was +created from scratch with 'new playlist'), this option is required. """.strip()), ], - help=self.__doc__) + help=self.__doc__, plugin=plugin) - def _run(inqueue, outqueue, params): - params['playlist'].save(params['output']) + def _run(self, hooke, inqueue, outqueue, params): + self._playlist(hooke, params).save(params['output']) -class LoadCommand (Command): + +class LoadCommand (PlaylistAddingCommand): """Load a playlist. """ - def __init__(self): + def __init__(self, plugin): super(LoadCommand, self).__init__( name='load playlist', arguments=[ @@ -327,30 +236,31 @@ class LoadCommand (Command): help=""" File name for the input playlist. """.strip()), - Argument(name='digests', type='digest', optional=False, - count=-1, + Argument(name='drivers', type='driver', optional=True, + count=-1, callback=all_drivers_callback, help=""" -Digests for loading curves. +Drivers for loading curves. """.strip()), ], - help=self.__doc__) + help=self.__doc__, plugin=plugin) - def _run(inqueue, outqueue, params): + def _run(self, hooke, inqueue, outqueue, params): p = FilePlaylist(drivers=params['drivers'], path=params['input']) - p.load() + p.load(hooke=hooke) + playlist_names = [playlist.name for playlist in hooke.playlists] + if p.name in playlist_names or p.name == None: + p.name = params['output playlist'] # HACK: override input name. How to tell if it is callback-generated? + self._set_playlist(hooke, params, p) outqueue.put(p) -class AddCommand (Command): + +class AddCommand (PlaylistCommand): """Add a curve to a playlist. """ - def __init__(self): + def __init__(self, plugin): super(AddCommand, self).__init__( name='add curve to playlist', arguments=[ - Argument(name='playlist', type='playlist', optional=False, - help=""" -:class:``hooke.plugin.playlist.Playlist`` to act on. -""".strip()), Argument(name='input', type='file', optional=False, help=""" File name for the input :class:`hooke.curve.Curve`. @@ -360,51 +270,143 @@ File name for the input :class:`hooke.curve.Curve`. Additional information for the input :class:`hooke.curve.Curve`. """.strip()), ], - help=self.__doc__) + help=self.__doc__, plugin=plugin) + + def _run(self, hooke, inqueue, outqueue, params): + self._playlist(hooke, params).append_curve_by_path( + params['input'], params['info'], hooke=hooke) + + +class AddGlobCommand (PlaylistCommand): + """Add curves to a playlist with file globbing. + + Adding lots of files one at a time can be tedious. With this + command you can use globs (`data/curves/*.dat`) to add curves + for all matching files at once. + """ + def __init__(self, plugin): + super(AddGlobCommand, self).__init__( + name='glob curves to playlist', + arguments=[ + Argument(name='input', type='string', optional=False, + help=""" +File name glob for the input :class:`hooke.curve.Curve`. +""".strip()), + Argument(name='info', type='dict', optional=True, + help=""" +Additional information for the input :class:`hooke.curve.Curve`. +""".strip()), + ], + help=self.__doc__, plugin=plugin) - def _run(inqueue, outqueue, params): - params['playlist'].append_curve_by_path(params['input'], - params['info']) + def _run(self, hooke, inqueue, outqueue, params): + p = self._playlist(hooke, params) + for path in sorted(glob.glob(params['input'])): + p.append_curve_by_path(path, params['info'], hooke=hooke) + outqueue.put(p[-1]) -class RemoveCommand (Command): +class RemoveCommand (PlaylistCommand): """Remove a curve from a playlist. """ - def __init__(self): + def __init__(self, plugin): super(RemoveCommand, self).__init__( name='remove curve from playlist', arguments=[ - Argument(name='playlist', type='playlist', optional=False, - help=""" -:class:``hooke.plugin.playlist.Playlist`` to act on. -""".strip()), Argument(name='index', type='int', optional=False, help=""" Index of target curve. """.strip()), ], - help=self.__doc__) + help=self.__doc__, plugin=plugin) - def _run(inqueue, outqueue, params): - params['playlist'].pop(params['index']) - params['playlist'].jump(params._index) + def _run(self, hooke, inqueue, outqueue, params): + self._playlist(hooke, params).pop(params['index']) + self._playlist(hooke, params).jump(params.index()) -class FilterCommand (Command): - """Create a subset playlist via a selection function. + +class ApplyCommandStack (PlaylistCommand): + """Apply a :class:`~hooke.command_stack.CommandStack` to each + curve in a playlist. + + TODO: discuss `evaluate`. """ - def __init__(self): - super(FilterCommand, self).__init__( - name='filter playlist', + def __init__(self, plugin): + super(ApplyCommandStack, self).__init__( + name='apply command stack', arguments=[ - Argument(name='playlist', type='playlist', optional=False, + Argument(name='commands', type='command stack', optional=False, help=""" -:class:``hooke.plugin.playlist.Playlist`` to act on. +Command stack to apply to each curve. """.strip()), - Argument(name='filter', type='function', optional=False, + Argument(name='evaluate', type='bool', default=False, help=""" -Function returning `True` for "good" curves. +Evaluate the applied command stack immediately. """.strip()), ], - help=self.__doc__) + help=self.__doc__, plugin=plugin) + + def _run(self, hooke, inqueue, outqueue, params): + if len(params['commands']) == 0: + return + p = self._playlist(hooke, params) + if params['evaluate'] == True: + for curve in p.items(): + for command in params['commands']: + curve.command_stack.execute_command(hooke, command) + curve.command_stack.append(command) + else: + for curve in p: + curve.command_stack.extend(params['commands']) + curve.unload() # force command stack execution on next access. + + +class FilterCommand (PlaylistAddingCommand, PlaylistCommand): + """Create a subset playlist via a selection function. - def _run(inqueue, outqueue, params): - p = params['playlist'].filter(params['filter']) + Removing lots of curves one at a time can be tedious. With this + command you can use a function `filter` to select the curves you + wish to keep. + + Notes + ----- + There are issues with pickling functions bound to class + attributes, because the pickle module doesn't know where those + functions were originally defined (where it should point the + loader). Because of this, subclasses with hard-coded filter + functions are encouraged to define their filter function as a + method of their subclass. See, for example, + :meth:`NoteFilterCommand.filter`. + """ + def __init__(self, plugin, name='filter playlist'): + super(FilterCommand, self).__init__( + name=name, help=self.__doc__, plugin=plugin) + if not hasattr(self, 'filter'): + self.arguments.append( + Argument(name='filter', type='function', optional=False, + help=""" +Function returning `True` for "good" curves. +`filter(curve, hooke, inqueue, outqueue, params) -> True/False`. +""".strip())) + + def _run(self, hooke, inqueue, outqueue, params): + if not hasattr(self, 'filter'): + filter_fn = params['filter'] + else: + filter_fn = self.filter + p = self._playlist(hooke, params).filter(filter_fn, + hooke=hooke, inqueue=inqueue, outqueue=outqueue, params=params) + p.name = params['name'] + if hasattr(p, 'path') and p.path != None: + p.set_path(os.path.join(os.path.dirname(p.path), p.name)) + self._set_playlist(hooke, params, p) outqueue.put(p) + + +class NoteFilterCommand (FilterCommand): + """Create a subset playlist of curves with `.info['note'] != None`. + """ + def __init__(self, plugin): + super(NoteFilterCommand, self).__init__( + plugin, name='note filter playlist') + + def filter(self, curve, hooke, inqueue, outqueue, params): + return 'note' in curve.info and curve.info['note'] != None