1 # Copyright (C) 2010 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.
30 if False: # YAML dump debugging code
31 """To help isolate data types etc. that give YAML problems.
33 This is usually caused by external C modules (e.g. numpy) that
34 define new types (e.g. numpy.dtype) which YAML cannot inspect.
36 import yaml.representer
38 def ignore_aliases(data):
39 print data, type(data)
41 if data in [None, ()]:
43 if isinstance(data, (str, unicode, bool, int, float)):
45 yaml.representer.SafeRepresenter.ignore_aliases = staticmethod(
49 from yaml.representer import RepresenterError
51 from . import curve as curve
52 from .util.itertools import reverse_enumerate
55 class NoteIndexList (list):
56 """A list that keeps track of a "current" item and additional notes.
58 :attr:`index` (i.e. "bookmark") is the index of the currently
59 current curve. Also keep a :class:`dict` of additional information
62 def __init__(self, name=None):
63 super(NoteIndexList, self).__init__()
67 self._set_ignored_attrs()
70 return str(self.__unicode__())
72 def __unicode__(self):
73 return u'<%s %s>' % (self.__class__.__name__, self.name)
78 def _set_ignored_attrs(self):
79 self._ignored_attrs = ['_ignored_attrs', '_default_attrs']
80 self._default_attrs = {
84 def __getstate__(self):
85 state = dict(self.__dict__)
86 for key in self._ignored_attrs:
89 for key,value in self._default_attrs.items():
90 if key in state and state[key] == value:
92 assert 'items' not in state
94 self._assert_clean_state(self, state)
95 for item in self: # save curves and their attributes
96 item_state = self._item_getstate(item)
97 self._assert_clean_state(item, item_state)
98 state['items'].append(item_state)
101 def __setstate__(self, state):
102 self._set_ignored_attrs()
103 for key,value in self._default_attrs.items():
104 setattr(self, key, value)
105 for key,value in state.items():
108 setattr(self, key, value)
109 for item_state in state['items']:
110 self.append(self._item_setstate(item_state))
112 def _item_getstate(self, item):
115 def _item_setstate(self, state):
118 def _assert_clean_state(self, owner, state):
119 for k,v in state.items():
120 if k == 'drivers': # HACK. Need better driver serialization.
124 except RepresenterError, e:
125 raise NotImplementedError(
126 'cannot convert %s.%s = %s (%s) to YAML\n%s'
127 % (owner.__class__.__name__, k, v, type(v), e))
129 def _setup_item(self, item):
130 """Perform any required initialization before returning an item.
134 def index(self, value=None, *args, **kwargs):
135 """Extend `list.index`, returning the current index if `value`
140 return super(NoteIndexList, self).index(value, *args, **kwargs)
145 item = self[self._index]
146 self._setup_item(item)
149 def jump(self, index):
153 self._index = index % len(self)
156 self.jump(self._index + 1)
159 self.jump(self._index - 1)
161 def items(self, reverse=False):
162 """Iterate through `self` calling `_setup_item` on each item
167 Updates :attr:`_index` during the iteration so
168 :func:`~hooke.plugin.curve.current_curve_callback` works as
169 expected in :class:`~hooke.command.Command`\s called from
170 :class:`~hooke.plugin.playlist.ApplyCommand`. After the
171 iteration completes, :attr:`_index` is restored to its
177 items = reverse_enumerate(self)
179 items = enumerate(self)
182 self._setup_item(item)
186 def filter(self, keeper_fn=lambda item:True, load_curves=True,
188 c = copy.deepcopy(self)
189 if load_curves == True:
190 items = c.items(reverse=True)
194 if keeper_fn(item, *args, **kwargs) != True:
196 try: # attempt to maintain the same current item
197 c._index = c.index(self.current())
203 class Playlist (NoteIndexList):
204 """A :class:`NoteIndexList` of :class:`hooke.curve.Curve`\s.
206 Keeps a list of :attr:`drivers` for loading curves.
208 def __init__(self, drivers, name=None):
209 super(Playlist, self).__init__(name=name)
210 self.drivers = drivers
211 self._max_loaded = 100 # curves to hold in memory simultaneously.
213 def _set_ignored_attrs(self):
214 super(Playlist, self)._set_ignored_attrs()
215 self._ignored_attrs.extend([
216 '_item_ignored_attrs', '_item_default_attrs',
218 self._item_ignored_attrs = ['data']
219 self._item_default_attrs = {
225 self._loaded = [] # List of loaded curves, see :meth:`._setup_item`.
227 def _item_getstate(self, item):
228 assert isinstance(item, curve.Curve), type(item)
229 state = item.__getstate__()
230 for key in self._item_ignored_attrs:
233 for key,value in self._item_default_attrs.items():
234 if key in state and state[key] == value:
238 def _item_setstate(self, state):
239 for key,value in self._item_default_attrs.items():
242 item = curve.Curve(path=None)
243 item.__setstate__(state)
246 def append_curve_by_path(self, path, info=None, identify=True, hooke=None):
247 path = os.path.normpath(path)
248 c = curve.Curve(path, info=info)
251 c.identify(self.drivers)
255 def _setup_item(self, curve):
256 if curve != None and curve not in self._loaded:
257 if curve not in self:
259 if curve.driver == None:
260 c.identify(self.drivers)
261 if curve.data == None:
263 self._loaded.append(curve)
264 if len(self._loaded) > self._max_loaded:
265 oldest = self._loaded.pop(0)
269 class FilePlaylist (Playlist):
270 """A file-backed :class:`Playlist`.
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
280 def _set_ignored_attrs(self):
281 super(FilePlaylist, self)._set_ignored_attrs()
282 self._ignored_attrs.append('_digest')
285 def __getstate__(self):
286 state = super(FilePlaylist, self).__getstate__()
287 assert 'version' not in state, state
288 state['version'] = self.version
291 def __setstate__(self, state):
292 assert('version') in state, state
293 version = state.pop('version')
294 assert version == FilePlaylist.version, (
295 'invalid version %s (%s) != %s (%s)'
296 % (version, type(version),
297 FilePlaylist.version, type(FilePlaylist.version)))
298 super(FilePlaylist, self).__setstate__(state)
300 def _item_getstate(self, item):
301 state = super(FilePlaylist, self)._item_getstate(item)
302 if state.get('path', None) != None:
303 path = os.path.abspath(os.path.expanduser(state['path']))
304 if self._relative_curve_paths == True:
305 path = os.path.relpath(path, self._base_path)
309 def _item_setstate(self, state):
310 item = super(FilePlaylist, self)._item_setstate(state)
312 item.set_path(os.path.join(self._base_path, state['path']))
315 def set_path(self, path):
317 if self._base_path == None:
318 self._base_path = os.getcwd()
320 if not path.endswith('.hkp'):
323 self._base_path = os.path.dirname(os.path.abspath(
324 os.path.expanduser(self.path)))
325 if self.name == None:
326 self.name = os.path.basename(path)
328 def append_curve_by_path(self, path, *args, **kwargs):
329 if self._base_path != None:
330 path = os.path.join(self._base_path, path)
331 super(FilePlaylist, self).append_curve_by_path(path, *args, **kwargs)
334 return self.digest() == self._digest
337 r"""Compute the sha1 digest of the flattened playlist
343 >>> root_path = os.path.sep + 'path'
344 >>> p = FilePlaylist(drivers=[],
345 ... path=os.path.join(root_path, 'to','playlist'))
346 >>> p.info['note'] = 'An example playlist'
347 >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'one'))
348 >>> c.info['note'] = 'The first curve'
350 >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'two'))
351 >>> c.info['note'] = 'The second curve'
354 '\xa1\x1ax\xb1|\x84uA\xe4\x1d\xbf`\x004|\x82\xc2\xdd\xc1\x9e'
356 string = self.flatten()
357 return hashlib.sha1(string).digest()
360 """Create a string representation of the playlist.
362 A playlist is a YAML document with the following minimal syntax::
366 - path: picoforce.000
367 - path: picoforce.001
369 Relative paths are interpreted relative to the location of the
375 >>> from .engine import CommandMessage
377 >>> root_path = os.path.sep + 'path'
378 >>> p = FilePlaylist(drivers=[],
379 ... path=os.path.join(root_path, 'to','playlist'))
380 >>> p.info['note'] = 'An example playlist'
381 >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'one'))
382 >>> c.info['note'] = 'The first curve'
384 >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'two'))
385 >>> c.info['attr with spaces'] = 'The second curve\\nwith endlines'
386 >>> c.command_stack.extend([
387 ... CommandMessage('command A', {'arg 0':0, 'arg 1':'X'}),
388 ... CommandMessage('command B', {'arg 0':1, 'arg 1':'Y'}),
391 >>> print p.flatten() # doctest: +REPORT_UDIFF
392 # Hooke playlist version 0.2
396 _relative_curve_paths: true
398 info: {note: An example playlist}
400 - info: {note: The first curve}
403 - command_stack: !!python/object/new:hooke.command_stack.CommandStack
405 - !!python/object:hooke.engine.CommandMessage
406 arguments: {arg 0: 0, arg 1: X}
408 - !!python/object:hooke.engine.CommandMessage
409 arguments: {arg 0: 1, arg 1: Y}
411 info: {attr with spaces: 'The second curve
417 path: /path/to/playlist.hkp
420 >>> p._relative_curve_paths = False
421 >>> print p.flatten() # doctest: +REPORT_UDIFF
422 # Hooke playlist version 0.2
426 _relative_curve_paths: false
428 info: {note: An example playlist}
430 - info: {note: The first curve}
432 path: /path/to/curve/one
433 - command_stack: !!python/object/new:hooke.command_stack.CommandStack
435 - !!python/object:hooke.engine.CommandMessage
436 arguments: {arg 0: 0, arg 1: X}
438 - !!python/object:hooke.engine.CommandMessage
439 arguments: {arg 0: 1, arg 1: Y}
441 info: {attr with spaces: 'The second curve
445 path: /path/to/curve/two
447 path: /path/to/playlist.hkp
451 yaml_string = yaml.dump(self.__getstate__(), allow_unicode=True)
452 return ('# Hooke playlist version %s\n' % self.version) + yaml_string
454 def from_string(self, string):
455 u"""Load a playlist from a string.
462 >>> string = '''# Hooke playlist version 0.2
465 ... - path: picoforce.000
466 ... - path: picoforce.001
468 >>> p = FilePlaylist(drivers=[],
469 ... path=os.path.join('/path', 'to', 'my', 'playlist'))
470 >>> p.from_string(string)
473 /path/to/my/picoforce.000
474 /path/to/my/picoforce.001
476 More complicated example.
478 >>> string = '''# Hooke playlist version 0.2
479 ... _base_path: /path/to
483 ... _relative_curve_paths: true
484 ... info: {note: An example playlist}
486 ... - info: {note: The first curve}
488 ... - command_stack: !!python/object/new:hooke.command_stack.CommandStack
490 ... - !!python/object:hooke.engine.CommandMessage
491 ... arguments: {arg 0: 0, arg 1: X}
492 ... command: command A
493 ... - !!python/object:hooke.engine.CommandMessage
494 ... arguments: {arg 0: 1, arg 1: Y}
495 ... command: command B
496 ... info: {attr with spaces: 'The second curve
501 ... name: playlist.hkp
502 ... path: /path/to/playlist.hkp
505 >>> p = FilePlaylist(drivers=[],
506 ... path=os.path.join('path', 'to', 'my', 'playlist'))
507 >>> p.from_string(string)
511 {'note': 'An example playlist'}
513 ... print curve.name, curve.path
514 one /path/to/curve/one
515 two /path/to/curve/two
516 >>> p[-1].info['attr with spaces']
517 'The second curve\\nwith endlines'
518 >>> type(p[-1].command_stack)
519 <class 'hooke.command_stack.CommandStack'>
520 >>> p[-1].command_stack # doctest: +NORMALIZE_WHITESPACE
521 [<CommandMessage command A {arg 0: 0, arg 1: X}>,
522 <CommandMessage command B {arg 0: 1, arg 1: Y}>]
524 state = yaml.load(string)
525 self.__setstate__(state)
527 def save(self, path=None, makedirs=True):
528 """Saves the playlist to a YAML file.
531 dirname = os.path.dirname(self.path) or '.'
532 if makedirs == True and not os.path.isdir(dirname):
534 with open(self.path, 'w') as f:
535 f.write(self.flatten())
536 self._digest = self.digest()
538 def load(self, path=None, identify=True, hooke=None):
539 """Load a playlist from a file.
542 with open(self.path, 'r') as f:
544 self.from_string(text)
545 self._digest = self.digest()
547 curve.set_hooke(hooke)
549 curve.identify(self.drivers)
552 class Playlists (NoteIndexList):
553 """A :class:`NoteIndexList` of :class:`FilePlaylist`\s.
555 def __init__(self, *arg, **kwargs):
556 super(Playlists, self).__init__(*arg, **kwargs)
558 def _item_getstate(self, item):
559 assert isinstance(item, FilePlaylist), type(item)
560 return item.__getstate__()
562 def _item_setstate(self, state):
563 item = FilePlaylist(drivers=[])
564 item.__setstate__(state)