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 under the
6 # terms of the GNU Lesser General Public License as published by the Free
7 # Software Foundation, either version 3 of the License, or (at your option) any
10 # Hooke is distributed in the hope that it will be useful, but WITHOUT ANY
11 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12 # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
15 # You should have received a copy of the GNU Lesser General Public License
16 # along with Hooke. If not, see <http://www.gnu.org/licenses/>.
18 """The `playlist` module provides a :class:`Playlist` and its subclass
19 :class:`FilePlaylist` for manipulating lists of
20 :class:`hooke.curve.Curve`\s.
30 from yaml.representer import RepresenterError
32 from .command_stack import CommandStack
33 from .curve import Curve
34 from .util.itertools import reverse_enumerate
37 class NoteIndexList (list):
38 """A list that keeps track of a "current" item and additional notes.
40 :attr:`index` (i.e. "bookmark") is the index of the currently
41 current curve. Also keep a :class:`dict` of additional information
44 def __init__(self, name=None):
45 super(NoteIndexList, self).__init__()
46 self._set_default_attrs()
47 self.__setstate__({'name': name})
50 return str(self.__unicode__())
52 def __unicode__(self):
53 return u'<%s %s>' % (self.__class__.__name__, self.name)
58 def _set_default_attrs(self):
59 self._default_attrs = {
65 def __getstate__(self):
66 return self.__dict__.copy()
68 def __setstate__(self, state):
69 self._set_default_attrs()
72 self.__dict__.update(self._default_attrs)
74 self.__dict__.update(state)
76 print state, type(state), e
77 if self.info in [None, {}]:
80 def _setup_item(self, item):
81 """Perform any required initialization before returning an item.
85 def index(self, value=None, *args, **kwargs):
86 """Extend `list.index`, returning the current index if `value`
90 if self._index >= len(self): # perhaps items have been popped
91 self._index = len(self) - 1
93 return super(NoteIndexList, self).index(value, *args, **kwargs)
95 def current(self, load=True):
98 item = self[self._index]
100 self._setup_item(item)
103 def jump(self, index):
107 self._index = index % len(self)
110 self.jump(self._index + 1)
113 self.jump(self._index - 1)
115 def items(self, reverse=False):
116 """Iterate through `self` calling `_setup_item` on each item
121 Updates :attr:`_index` during the iteration so
122 :func:`~hooke.plugin.curve.current_curve_callback` works as
123 expected in :class:`~hooke.command.Command`\s called from
124 :class:`~hooke.plugin.playlist.ApplyCommand`. After the
125 iteration completes, :attr:`_index` is restored to its
131 # could iterate through `c` if current_curve_callback()
132 # would work, but `c` is not bound to the local `hooke`,
133 # so curent_playlist_callback cannot point to it.
134 items = reverse_enumerate(self)
136 items = enumerate(self)
139 self._setup_item(item)
143 def filter(self, keeper_fn=lambda item:True, load_curves=True,
146 if load_curves == True:
147 items = self.items(reverse=True)
149 items = reversed(self)
151 if keeper_fn(item, *args, **kwargs) != True:
153 try: # attempt to maintain the same current item
154 c._index = c.index(self.current())
160 class Playlist (NoteIndexList):
161 """A :class:`NoteIndexList` of :class:`hooke.Curve`\s.
163 Keeps a list of :attr:`drivers` for loading curves.
165 def __init__(self, drivers, name=None):
166 super(Playlist, self).__init__(name=name)
167 self.drivers = drivers
169 def _set_default_attrs(self):
170 super(Playlist, self)._set_default_attrs()
171 self._default_attrs['drivers'] = []
172 # List of loaded curves, see :meth:`._setup_item`.
173 self._default_attrs['_loaded'] = []
174 self._default_attrs['_max_loaded'] = 100 # curves to hold in memory simultaneously.
176 def __setstate__(self, state):
177 super(Playlist, self).__setstate__(state)
178 if self.drivers in [None, {}]:
180 if self._loaded in [None, {}]:
183 def append_curve(self, curve):
186 def append_curve_by_path(self, path, info=None, identify=True, hooke=None):
187 path = os.path.normpath(path)
188 c = Curve(path, info=info)
191 c.identify(self.drivers)
195 def _setup_item(self, curve):
196 if curve != None and curve not in self._loaded:
197 if curve not in self:
199 if curve.driver == None:
200 c.identify(self.drivers)
201 if curve.data == None or max([d.size for d in curve.data]) == 0:
203 self._loaded.append(curve)
204 if len(self._loaded) > self._max_loaded:
205 oldest = self._loaded.pop(0)
208 def unload(self, curve):
209 "Inverse of `._setup_item`."
212 self._loaded.remove(curve)
217 def playlist_path(path, expand=False):
218 """Normalize playlist path extensions.
222 >>> print playlist_path('playlist')
224 >>> print playlist_path('playlist.hkp')
226 >>> print playlist_path(None)
231 if not path.endswith('.hkp'):
234 path = os.path.abspath(os.path.expanduser(path))
238 class FilePlaylist (Playlist):
239 """A file-backed :class:`Playlist`.
244 >>> p = FilePlaylist(drivers=['Driver A', 'Driver B'])
245 >>> p.append(Curve('dummy/path/A'))
246 >>> p.append(Curve('dummy/path/B'))
248 The data-type is pickleable, to ensure we can move it between
249 processes with :class:`multiprocessing.Queue`\s.
252 >>> s = pickle.dumps(p)
253 >>> z = pickle.loads(s)
259 ['Driver A', 'Driver B']
261 The data-type is also YAMLable (see :mod:`hooke.util.yaml`).
270 ['Driver A', 'Driver B']
274 def __init__(self, drivers, name=None, path=None):
275 super(FilePlaylist, self).__init__(drivers, name)
276 self.path = self._base_path = None
278 self.relative_curve_paths = True
279 self._relative_curve_paths = False
281 def _set_default_attrs(self):
282 super(FilePlaylist, self)._set_default_attrs()
283 self._default_attrs['relative_curve_paths'] = True
284 self._default_attrs['_relative_curve_paths'] = False
285 self._default_attrs['_digest'] = None
287 def __getstate__(self):
288 state = super(FilePlaylist, self).__getstate__()
289 assert 'version' not in state, state
290 state['version'] = self.version
293 def __setstate__(self, state):
294 if 'version' in state:
295 version = state.pop('version')
296 assert version == FilePlaylist.version, (
297 'invalid version %s (%s) != %s (%s)'
298 % (version, type(version),
299 FilePlaylist.version, type(FilePlaylist.version)))
300 super(FilePlaylist, self).__setstate__(state)
302 def set_path(self, path):
303 orig_base_path = getattr(self, '_base_path', None)
305 if self._base_path == None:
306 self._base_path = os.getcwd()
308 path = playlist_path(path, expand=True)
310 self._base_path = os.path.dirname(self.path)
311 if self.name == None:
312 self.name = os.path.basename(path)
313 if self._base_path != orig_base_path:
314 self.update_curve_paths()
316 def update_curve_paths(self):
318 curve.set_path(self._curve_path(curve.path))
320 def _curve_path(self, path):
321 if self._base_path == None:
322 self._base_path = os.getcwd()
323 path = os.path.join(self._base_path, path)
324 if self._relative_curve_paths == True:
325 path = os.path.relpath(path, self._base_path)
328 def append_curve(self, curve):
329 curve.set_path(self._curve_path(curve.path))
330 super(FilePlaylist, self).append_curve(curve)
332 def append_curve_by_path(self, path, *args, **kwargs):
333 path = self._curve_path(path)
334 super(FilePlaylist, self).append_curve_by_path(path, *args, **kwargs)
337 return self.digest() == self._digest
340 r"""Compute the sha1 digest of the flattened playlist
346 >>> root_path = os.path.sep + 'path'
347 >>> p = FilePlaylist(drivers=[],
348 ... path=os.path.join(root_path, 'to','playlist'))
349 >>> p.info['note'] = 'An example playlist'
350 >>> c = Curve(os.path.join(root_path, 'to', 'curve', 'one'))
351 >>> c.info['note'] = 'The first curve'
352 >>> p.append_curve(c)
353 >>> c = Curve(os.path.join(root_path, 'to', 'curve', 'two'))
354 >>> c.info['note'] = 'The second curve'
355 >>> p.append_curve(c)
357 'f\xe26i\xb98i\x1f\xb61J7:\xf2\x8e\x1d\xde\xc3}g'
359 string = self.flatten()
360 return hashlib.sha1(string).digest()
363 """Create a string representation of the playlist.
365 A playlist is a YAML document with the following minimal syntax::
367 !!python/object/new:hooke.playlist.FilePlaylist
371 - !!python/object:hooke.curve.Curve
372 path: /path/to/curve/one
373 - !!python/object:hooke.curve.Curve
374 path: /path/to/curve/two
376 Relative paths are interpreted relative to the location of the
382 >>> from .engine import CommandMessage
384 >>> root_path = os.path.sep + 'path'
385 >>> p = FilePlaylist(drivers=[],
386 ... path=os.path.join(root_path, 'to','playlist'))
387 >>> p.info['note'] = 'An example playlist'
388 >>> c = Curve(os.path.join(root_path, 'to', 'curve', 'one'))
389 >>> c.info['note'] = 'The first curve'
390 >>> p.append_curve(c)
391 >>> c = Curve(os.path.join(root_path, 'to', 'curve', 'two'))
392 >>> c.info['attr with spaces'] = 'The second curve\\nwith endlines'
393 >>> c.command_stack.extend([
394 ... CommandMessage('command A', {'arg 0':0, 'arg 1':'X'}),
395 ... CommandMessage('command B', {'arg 0':1, 'curve':c}),
397 >>> p.append_curve(c)
398 >>> print p.flatten() # doctest: +REPORT_UDIFF
399 # Hooke playlist version 0.2
400 !!python/object/new:hooke.playlist.FilePlaylist
402 - !!python/object:hooke.curve.Curve
403 info: {note: The first curve}
406 - &id001 !!python/object:hooke.curve.Curve
407 command_stack: !!python/object/new:hooke.command_stack.CommandStack
409 - !!python/object:hooke.engine.CommandMessage
410 arguments: {arg 0: 0, arg 1: X}
412 explicit_user_call: true
413 - !!python/object:hooke.engine.CommandMessage
418 explicit_user_call: true
419 info: {attr with spaces: 'The second curve
426 info: {note: An example playlist}
428 path: /path/to/playlist.hkp
431 >>> p.relative_curve_paths = False
432 >>> print p.flatten() # doctest: +REPORT_UDIFF
433 # Hooke playlist version 0.2
434 !!python/object/new:hooke.playlist.FilePlaylist
436 - !!python/object:hooke.curve.Curve
437 info: {note: The first curve}
439 path: /path/to/curve/one
440 - &id001 !!python/object:hooke.curve.Curve
441 command_stack: !!python/object/new:hooke.command_stack.CommandStack
443 - !!python/object:hooke.engine.CommandMessage
444 arguments: {arg 0: 0, arg 1: X}
446 explicit_user_call: true
447 - !!python/object:hooke.engine.CommandMessage
452 explicit_user_call: true
453 info: {attr with spaces: 'The second curve
457 path: /path/to/curve/two
460 info: {note: An example playlist}
462 path: /path/to/playlist.hkp
463 relative_curve_paths: false
467 rcp = self._relative_curve_paths
468 self._relative_curve_paths = self.relative_curve_paths
469 self.update_curve_paths()
470 self._relative_curve_paths = rcp
471 digest = self._digest
472 self._digest = None # don't save the digest (recursive file).
473 yaml_string = yaml.dump(self, allow_unicode=True)
474 self._digest = digest
475 self.update_curve_paths()
476 return ('# Hooke playlist version %s\n' % self.version) + yaml_string
478 def save(self, path=None, makedirs=True):
479 """Saves the playlist to a YAML file.
482 dirname = os.path.dirname(self.path) or '.'
483 if makedirs == True and not os.path.isdir(dirname):
485 with open(self.path, 'w') as f:
486 f.write(self.flatten())
487 self._digest = self.digest()
490 def from_string(string):
491 u"""Load a playlist from a string.
498 >>> string = '''# Hooke playlist version 0.2
499 ... !!python/object/new:hooke.playlist.FilePlaylist
503 ... - !!python/object:hooke.curve.Curve
505 ... - !!python/object:hooke.curve.Curve
508 >>> p = from_string(string)
509 >>> p.set_path('/path/to/playlist')
511 ... print curve.name, curve.path
512 one /path/to/curve/one
513 two /path/to/curve/two
515 More complicated example.
517 >>> string = '''# Hooke playlist version 0.2
518 ... !!python/object/new:hooke.playlist.FilePlaylist
520 ... - !!python/object:hooke.curve.Curve
521 ... info: {note: The first curve}
523 ... path: /path/to/curve/one
524 ... - &id001 !!python/object:hooke.curve.Curve
525 ... command_stack: !!python/object/new:hooke.command_stack.CommandStack
527 ... - !!python/object:hooke.engine.CommandMessage
528 ... arguments: {arg 0: 0, arg 1: X}
529 ... command: command A
530 ... - !!python/object:hooke.engine.CommandMessage
534 ... command: command B
535 ... info: {attr with spaces: 'The second curve
539 ... path: /path/to/curve/two
541 ... _base_path: /path/to
543 ... info: {note: An example playlist}
544 ... name: playlist.hkp
545 ... path: /path/to/playlist.hkp
548 >>> p = from_string(string)
549 >>> p.set_path('/path/to/playlist')
553 {'note': 'An example playlist'}
555 ... print curve.name, curve.path
556 one /path/to/curve/one
557 two /path/to/curve/two
558 >>> p[-1].info['attr with spaces']
559 'The second curve\\nwith endlines'
560 >>> type(p[-1].command_stack)
561 <class 'hooke.command_stack.CommandStack'>
562 >>> p[0].command_stack
564 >>> type(p[0].command_stack)
565 <class 'hooke.command_stack.CommandStack'>
566 >>> p[-1].command_stack # doctest: +NORMALIZE_WHITESPACE
567 [<CommandMessage command A {arg 0: 0, arg 1: X}>,
568 <CommandMessage command B {arg 0: 1, curve: <Curve two>}>]
569 >>> type(p[1].command_stack)
570 <class 'hooke.command_stack.CommandStack'>
572 >>> c2.command_stack[-1].arguments['curve'] == c2
575 return yaml.load(string)
577 def load(path=None, drivers=None, identify=True, hooke=None):
578 """Load a playlist from a file.
580 path = playlist_path(path, expand=True)
581 with open(path, 'r') as f:
583 playlist = from_string(text)
584 playlist.set_path(path)
585 playlist._digest = playlist.digest()
587 playlist.drivers = drivers
588 playlist.set_path(path)
589 for curve in playlist:
590 curve.set_hooke(hooke)
592 curve.identify(playlist.drivers)
596 class Playlists (NoteIndexList):
597 """A :class:`NoteIndexList` of :class:`FilePlaylist`\s.