1 # Copyright (C) 2010-2012 W. Trevor King <wking@drexel.edu>
3 # This file is part of Hooke.
5 # Hooke is free software: you can redistribute it and/or modify it
6 # under the terms of the GNU Lesser General Public License as
7 # published by the Free Software Foundation, either version 3 of the
8 # License, or (at your option) any later version.
10 # Hooke is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
12 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General
13 # Public License for more details.
15 # You should have received a copy of the GNU Lesser General Public
16 # License along with Hooke. If not, see
17 # <http://www.gnu.org/licenses/>.
19 """The `playlist` module provides a :class:`Playlist` and its subclass
20 :class:`FilePlaylist` for manipulating lists of
21 :class:`hooke.curve.Curve`\s.
31 from yaml.representer import RepresenterError
33 from .command_stack import CommandStack
34 from .curve import Curve
35 from .util.itertools import reverse_enumerate
38 class NoteIndexList (list):
39 """A list that keeps track of a "current" item and additional notes.
41 :attr:`index` (i.e. "bookmark") is the index of the currently
42 current curve. Also keep a :class:`dict` of additional information
45 def __init__(self, name=None):
46 super(NoteIndexList, self).__init__()
47 self._set_default_attrs()
48 self.__setstate__({'name': name})
51 return str(self.__unicode__())
53 def __unicode__(self):
54 return u'<%s %s>' % (self.__class__.__name__, self.name)
59 def _set_default_attrs(self):
60 self._default_attrs = {
66 def __getstate__(self):
67 return self.__dict__.copy()
69 def __setstate__(self, state):
70 self._set_default_attrs()
73 self.__dict__.update(self._default_attrs)
75 self.__dict__.update(state)
77 print state, type(state), e
78 if self.info in [None, {}]:
81 def _setup_item(self, item):
82 """Perform any required initialization before returning an item.
86 def index(self, value=None, *args, **kwargs):
87 """Extend `list.index`, returning the current index if `value`
91 if self._index >= len(self): # perhaps items have been popped
92 self._index = len(self) - 1
94 return super(NoteIndexList, self).index(value, *args, **kwargs)
96 def current(self, load=True):
99 item = self[self._index]
101 self._setup_item(item)
104 def jump(self, index):
108 self._index = index % len(self)
111 self.jump(self._index + 1)
114 self.jump(self._index - 1)
116 def items(self, reverse=False):
117 """Iterate through `self` calling `_setup_item` on each item
122 Updates :attr:`_index` during the iteration so
123 :func:`~hooke.plugin.curve.current_curve_callback` works as
124 expected in :class:`~hooke.command.Command`\s called from
125 :class:`~hooke.plugin.playlist.ApplyCommand`. After the
126 iteration completes, :attr:`_index` is restored to its
132 # could iterate through `c` if current_curve_callback()
133 # would work, but `c` is not bound to the local `hooke`,
134 # so curent_playlist_callback cannot point to it.
135 items = reverse_enumerate(self)
137 items = enumerate(self)
140 self._setup_item(item)
144 def filter(self, keeper_fn=lambda item:True, load_curves=True,
147 if load_curves == True:
148 items = self.items(reverse=True)
150 items = reversed(self)
152 if keeper_fn(item, *args, **kwargs) != True:
154 try: # attempt to maintain the same current item
155 c._index = c.index(self.current())
161 class Playlist (NoteIndexList):
162 """A :class:`NoteIndexList` of :class:`hooke.Curve`\s.
164 Keeps a list of :attr:`drivers` for loading curves.
166 def __init__(self, drivers, name=None):
167 super(Playlist, self).__init__(name=name)
168 self.drivers = drivers
170 def _set_default_attrs(self):
171 super(Playlist, self)._set_default_attrs()
172 self._default_attrs['drivers'] = []
173 # List of loaded curves, see :meth:`._setup_item`.
174 self._default_attrs['_loaded'] = []
175 self._default_attrs['_max_loaded'] = 100 # curves to hold in memory simultaneously.
177 def __setstate__(self, state):
178 super(Playlist, self).__setstate__(state)
179 if self.drivers in [None, {}]:
181 if self._loaded in [None, {}]:
184 def append_curve(self, curve):
187 def append_curve_by_path(self, path, info=None, identify=True, hooke=None):
188 path = os.path.normpath(path)
189 c = Curve(path, info=info)
192 c.identify(self.drivers)
196 def _setup_item(self, curve):
197 if curve != None and curve not in self._loaded:
198 if curve not in self:
200 if curve.driver == None:
201 c.identify(self.drivers)
202 if curve.data == None or max([d.size for d in curve.data]) == 0:
204 self._loaded.append(curve)
205 if len(self._loaded) > self._max_loaded:
206 oldest = self._loaded.pop(0)
209 def unload(self, curve):
210 "Inverse of `._setup_item`."
213 self._loaded.remove(curve)
218 def playlist_path(path, expand=False):
219 """Normalize playlist path extensions.
223 >>> print playlist_path('playlist')
225 >>> print playlist_path('playlist.hkp')
227 >>> print playlist_path(None)
232 if not path.endswith('.hkp'):
235 path = os.path.abspath(os.path.expanduser(path))
239 class FilePlaylist (Playlist):
240 """A file-backed :class:`Playlist`.
245 >>> p = FilePlaylist(drivers=['Driver A', 'Driver B'])
246 >>> p.append(Curve('dummy/path/A'))
247 >>> p.append(Curve('dummy/path/B'))
249 The data-type is pickleable, to ensure we can move it between
250 processes with :class:`multiprocessing.Queue`\s.
253 >>> s = pickle.dumps(p)
254 >>> z = pickle.loads(s)
260 ['Driver A', 'Driver B']
262 The data-type is also YAMLable (see :mod:`hooke.util.yaml`).
271 ['Driver A', 'Driver B']
275 def __init__(self, drivers, name=None, path=None):
276 super(FilePlaylist, self).__init__(drivers, name)
277 self.path = self._base_path = None
279 self.relative_curve_paths = True
280 self._relative_curve_paths = False
282 def _set_default_attrs(self):
283 super(FilePlaylist, self)._set_default_attrs()
284 self._default_attrs['relative_curve_paths'] = True
285 self._default_attrs['_relative_curve_paths'] = False
286 self._default_attrs['_digest'] = None
288 def __getstate__(self):
289 state = super(FilePlaylist, self).__getstate__()
290 assert 'version' not in state, state
291 state['version'] = self.version
294 def __setstate__(self, state):
295 if 'version' in state:
296 version = state.pop('version')
297 assert version == FilePlaylist.version, (
298 'invalid version %s (%s) != %s (%s)'
299 % (version, type(version),
300 FilePlaylist.version, type(FilePlaylist.version)))
301 super(FilePlaylist, self).__setstate__(state)
303 def set_path(self, path):
304 orig_base_path = getattr(self, '_base_path', None)
306 if self._base_path == None:
307 self._base_path = os.getcwd()
309 path = playlist_path(path, expand=True)
311 self._base_path = os.path.dirname(self.path)
312 if self.name == None:
313 self.name = os.path.basename(path)
314 if self._base_path != orig_base_path:
315 self.update_curve_paths()
317 def update_curve_paths(self):
319 curve.set_path(self._curve_path(curve.path))
321 def _curve_path(self, path):
322 if self._base_path == None:
323 self._base_path = os.getcwd()
324 path = os.path.join(self._base_path, path)
325 if self._relative_curve_paths == True:
326 path = os.path.relpath(path, self._base_path)
329 def append_curve(self, curve):
330 curve.set_path(self._curve_path(curve.path))
331 super(FilePlaylist, self).append_curve(curve)
333 def append_curve_by_path(self, path, *args, **kwargs):
334 path = self._curve_path(path)
335 super(FilePlaylist, self).append_curve_by_path(path, *args, **kwargs)
338 return self.digest() == self._digest
341 r"""Compute the sha1 digest of the flattened playlist
347 >>> root_path = os.path.sep + 'path'
348 >>> p = FilePlaylist(drivers=[],
349 ... path=os.path.join(root_path, 'to','playlist'))
350 >>> p.info['note'] = 'An example playlist'
351 >>> c = Curve(os.path.join(root_path, 'to', 'curve', 'one'))
352 >>> c.info['note'] = 'The first curve'
353 >>> p.append_curve(c)
354 >>> c = Curve(os.path.join(root_path, 'to', 'curve', 'two'))
355 >>> c.info['note'] = 'The second curve'
356 >>> p.append_curve(c)
358 'f\xe26i\xb98i\x1f\xb61J7:\xf2\x8e\x1d\xde\xc3}g'
360 string = self.flatten()
361 return hashlib.sha1(string).digest()
364 """Create a string representation of the playlist.
366 A playlist is a YAML document with the following minimal syntax::
368 !!python/object/new:hooke.playlist.FilePlaylist
372 - !!python/object:hooke.curve.Curve
373 path: /path/to/curve/one
374 - !!python/object:hooke.curve.Curve
375 path: /path/to/curve/two
377 Relative paths are interpreted relative to the location of the
383 >>> from .engine import CommandMessage
385 >>> root_path = os.path.sep + 'path'
386 >>> p = FilePlaylist(drivers=[],
387 ... path=os.path.join(root_path, 'to','playlist'))
388 >>> p.info['note'] = 'An example playlist'
389 >>> c = Curve(os.path.join(root_path, 'to', 'curve', 'one'))
390 >>> c.info['note'] = 'The first curve'
391 >>> p.append_curve(c)
392 >>> c = Curve(os.path.join(root_path, 'to', 'curve', 'two'))
393 >>> c.info['attr with spaces'] = 'The second curve\\nwith endlines'
394 >>> c.command_stack.extend([
395 ... CommandMessage('command A', {'arg 0':0, 'arg 1':'X'}),
396 ... CommandMessage('command B', {'arg 0':1, 'curve':c}),
398 >>> p.append_curve(c)
399 >>> print p.flatten() # doctest: +REPORT_UDIFF
400 # Hooke playlist version 0.2
401 !!python/object/new:hooke.playlist.FilePlaylist
403 - !!python/object:hooke.curve.Curve
404 info: {note: The first curve}
407 - &id001 !!python/object:hooke.curve.Curve
408 command_stack: !!python/object/new:hooke.command_stack.CommandStack
410 - !!python/object:hooke.engine.CommandMessage
411 arguments: {arg 0: 0, arg 1: X}
413 explicit_user_call: true
414 - !!python/object:hooke.engine.CommandMessage
419 explicit_user_call: true
420 info: {attr with spaces: 'The second curve
427 info: {note: An example playlist}
429 path: /path/to/playlist.hkp
432 >>> p.relative_curve_paths = False
433 >>> print p.flatten() # doctest: +REPORT_UDIFF
434 # Hooke playlist version 0.2
435 !!python/object/new:hooke.playlist.FilePlaylist
437 - !!python/object:hooke.curve.Curve
438 info: {note: The first curve}
440 path: /path/to/curve/one
441 - &id001 !!python/object:hooke.curve.Curve
442 command_stack: !!python/object/new:hooke.command_stack.CommandStack
444 - !!python/object:hooke.engine.CommandMessage
445 arguments: {arg 0: 0, arg 1: X}
447 explicit_user_call: true
448 - !!python/object:hooke.engine.CommandMessage
453 explicit_user_call: true
454 info: {attr with spaces: 'The second curve
458 path: /path/to/curve/two
461 info: {note: An example playlist}
463 path: /path/to/playlist.hkp
464 relative_curve_paths: false
468 rcp = self._relative_curve_paths
469 self._relative_curve_paths = self.relative_curve_paths
470 self.update_curve_paths()
471 self._relative_curve_paths = rcp
472 digest = self._digest
473 self._digest = None # don't save the digest (recursive file).
474 yaml_string = yaml.dump(self, allow_unicode=True)
475 self._digest = digest
476 self.update_curve_paths()
477 return ('# Hooke playlist version %s\n' % self.version) + yaml_string
479 def save(self, path=None, makedirs=True):
480 """Saves the playlist to a YAML file.
483 dirname = os.path.dirname(self.path) or '.'
484 if makedirs == True and not os.path.isdir(dirname):
486 with open(self.path, 'w') as f:
487 f.write(self.flatten())
488 self._digest = self.digest()
491 def from_string(string):
492 u"""Load a playlist from a string.
499 >>> string = '''# Hooke playlist version 0.2
500 ... !!python/object/new:hooke.playlist.FilePlaylist
504 ... - !!python/object:hooke.curve.Curve
506 ... - !!python/object:hooke.curve.Curve
509 >>> p = from_string(string)
510 >>> p.set_path('/path/to/playlist')
512 ... print curve.name, curve.path
513 one /path/to/curve/one
514 two /path/to/curve/two
516 More complicated example.
518 >>> string = '''# Hooke playlist version 0.2
519 ... !!python/object/new:hooke.playlist.FilePlaylist
521 ... - !!python/object:hooke.curve.Curve
522 ... info: {note: The first curve}
524 ... path: /path/to/curve/one
525 ... - &id001 !!python/object:hooke.curve.Curve
526 ... command_stack: !!python/object/new:hooke.command_stack.CommandStack
528 ... - !!python/object:hooke.engine.CommandMessage
529 ... arguments: {arg 0: 0, arg 1: X}
530 ... command: command A
531 ... - !!python/object:hooke.engine.CommandMessage
535 ... command: command B
536 ... info: {attr with spaces: 'The second curve
540 ... path: /path/to/curve/two
542 ... _base_path: /path/to
544 ... info: {note: An example playlist}
545 ... name: playlist.hkp
546 ... path: /path/to/playlist.hkp
549 >>> p = from_string(string)
550 >>> p.set_path('/path/to/playlist')
554 {'note': 'An example playlist'}
556 ... print curve.name, curve.path
557 one /path/to/curve/one
558 two /path/to/curve/two
559 >>> p[-1].info['attr with spaces']
560 'The second curve\\nwith endlines'
561 >>> type(p[-1].command_stack)
562 <class 'hooke.command_stack.CommandStack'>
563 >>> p[0].command_stack
565 >>> type(p[0].command_stack)
566 <class 'hooke.command_stack.CommandStack'>
567 >>> p[-1].command_stack # doctest: +NORMALIZE_WHITESPACE
568 [<CommandMessage command A {arg 0: 0, arg 1: X}>,
569 <CommandMessage command B {arg 0: 1, curve: <Curve two>}>]
570 >>> type(p[1].command_stack)
571 <class 'hooke.command_stack.CommandStack'>
573 >>> c2.command_stack[-1].arguments['curve'] == c2
576 return yaml.load(string)
578 def load(path=None, drivers=None, identify=True, hooke=None):
579 """Load a playlist from a file.
581 path = playlist_path(path, expand=True)
582 with open(path, 'r') as f:
584 playlist = from_string(text)
585 playlist.set_path(path)
586 playlist._digest = playlist.digest()
588 playlist.drivers = drivers
589 playlist.set_path(path)
590 for curve in playlist:
591 curve.set_hooke(hooke)
593 curve.identify(playlist.drivers)
597 class Playlists (NoteIndexList):
598 """A :class:`NoteIndexList` of :class:`FilePlaylist`\s.