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.
31 from yaml.representer import RepresenterError
33 from . import curve as 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__()
49 self._set_ignored_attrs()
52 return str(self.__unicode__())
54 def __unicode__(self):
55 return u'<%s %s>' % (self.__class__.__name__, self.name)
60 def _set_ignored_attrs(self):
61 self._ignored_attrs = ['_ignored_attrs', '_default_attrs']
62 self._default_attrs = {
66 def __getstate__(self):
67 state = dict(self.__dict__)
68 for key in self._ignored_attrs:
71 for key,value in self._default_attrs.items():
72 if key in state and state[key] == value:
74 assert 'items' not in state
76 self._assert_clean_state(self, state)
77 for item in self: # save curves and their attributes
78 item_state = self._item_getstate(item)
79 self._assert_clean_state(item, item_state)
80 state['items'].append(item_state)
83 def __setstate__(self, state):
84 self._set_ignored_attrs()
86 for key,value in self._default_attrs.items():
87 setattr(self, key, value)
88 for key,value in state.items():
91 setattr(self, key, value)
92 for item_state in state['items']:
93 self.append(self._item_setstate(item_state))
95 def _item_getstate(self, item):
98 def _item_setstate(self, state):
101 def _assert_clean_state(self, owner, state):
102 for k,v in state.items():
103 if k == 'drivers': # HACK. Need better driver serialization.
107 except RepresenterError, e:
108 raise NotImplementedError(
109 'cannot convert %s.%s = %s (%s) to YAML\n%s'
110 % (owner.__class__.__name__, k, v, type(v), e))
112 def _setup_item(self, item):
113 """Perform any required initialization before returning an item.
121 def index(self, value=None, *args, **kwargs):
122 """Extend `list.index`, returning the current index if `value`
127 return super(NoteIndexList, self).index(value, *args, **kwargs)
132 item = self[self._index]
133 self._setup_item(item)
136 def jump(self, index):
140 self._index = index % len(self)
143 self.jump(self._index + 1)
146 self.jump(self._index - 1)
148 def items(self, reverse=False):
149 """Iterate through `self` calling `_setup_item` on each item
154 Updates :attr:`_index` during the iteration so
155 :func:`~hooke.plugin.curve.current_curve_callback` works as
156 expected in :class:`~hooke.command.Command`\s called from
157 :class:`~hooke.plugin.playlist.ApplyCommand`. After the
158 iteration completes, :attr:`_index` is restored to its
164 items = reverse_enumerate(self)
166 items = enumerate(self)
169 self._setup_item(item)
173 def filter(self, keeper_fn=lambda item:True, load_curves=True,
175 c = copy.deepcopy(self)
176 if load_curves == True:
177 items = c.items(reverse=True)
181 if keeper_fn(item, *args, **kwargs) != True:
183 try: # attempt to maintain the same current item
184 c._index = c.index(self.current())
190 class Playlist (NoteIndexList):
191 """A :class:`NoteIndexList` of :class:`hooke.curve.Curve`\s.
193 Keeps a list of :attr:`drivers` for loading curves.
195 def __init__(self, drivers, name=None):
196 super(Playlist, self).__init__(name=name)
197 self.drivers = drivers
198 self._max_loaded = 100 # curves to hold in memory simultaneously.
200 def _set_ignored_attrs(self):
201 super(Playlist, self)._set_ignored_attrs()
202 self._ignored_attrs.extend([
203 '_item_ignored_attrs', '_item_default_attrs',
205 self._item_ignored_attrs = ['data']
206 self._item_default_attrs = {
212 self._loaded = [] # List of loaded curves, see :meth:`._setup_item`.
214 def _item_getstate(self, item):
215 assert isinstance(item, curve.Curve), type(item)
216 state = item.__getstate__()
217 for key in self._item_ignored_attrs:
220 for key,value in self._item_default_attrs.items():
221 if key in state and state[key] == value:
225 def _item_setstate(self, state):
226 for key,value in self._item_default_attrs.items():
229 item = curve.Curve(path=None)
230 item.__setstate__(state)
233 def append_curve_by_path(self, path, info=None, identify=True, hooke=None):
234 path = os.path.normpath(path)
235 c = curve.Curve(path, info=info)
238 c.identify(self.drivers)
242 def _setup_item(self, curve):
243 if curve != None and curve not in self._loaded:
244 if curve not in self:
246 if curve.driver == None:
247 c.identify(self.drivers)
248 if curve.data == None:
250 self._loaded.append(curve)
251 if len(self._loaded) > self._max_loaded:
252 oldest = self._loaded.pop(0)
256 class FilePlaylist (Playlist):
257 """A file-backed :class:`Playlist`.
261 def __init__(self, drivers, name=None, path=None):
262 super(FilePlaylist, self).__init__(drivers, name)
263 self.path = self._base_path = None
265 self._relative_curve_paths = True
267 def _set_ignored_attrs(self):
268 super(FilePlaylist, self)._set_ignored_attrs()
269 self._ignored_attrs.append('_digest')
272 def __getstate__(self):
273 state = super(FilePlaylist, self).__getstate__()
274 assert 'version' not in state, state
275 state['version'] = self.version
278 def __setstate__(self, state):
279 assert('version') in state, state
280 version = state.pop('version')
281 assert version == FilePlaylist.version, (
282 'invalid version %s (%s) != %s (%s)'
283 % (version, type(version),
284 FilePlaylist.version, type(FilePlaylist.version)))
285 super(FilePlaylist, self).__setstate__(state)
287 def _item_getstate(self, item):
288 state = super(FilePlaylist, self)._item_getstate(item)
289 if state.get('path', None) != None:
290 path = os.path.abspath(os.path.expanduser(state['path']))
291 if self._relative_curve_paths == True:
292 path = os.path.relpath(path, self._base_path)
296 def _item_setstate(self, state):
297 item = super(FilePlaylist, self)._item_setstate(state)
299 item.set_path(os.path.join(self._base_path, state['path']))
302 def set_path(self, path):
304 if self._base_path == None:
305 self._base_path = os.getcwd()
307 if not path.endswith('.hkp'):
310 self._base_path = os.path.dirname(os.path.abspath(
311 os.path.expanduser(self.path)))
312 if self.name == None:
313 self.name = os.path.basename(path)
315 def append_curve_by_path(self, path, *args, **kwargs):
316 if self._base_path != None:
317 path = os.path.join(self._base_path, path)
318 super(FilePlaylist, self).append_curve_by_path(path, *args, **kwargs)
321 return self.digest() == self._digest
324 r"""Compute the sha1 digest of the flattened playlist
330 >>> root_path = os.path.sep + 'path'
331 >>> p = FilePlaylist(drivers=[],
332 ... path=os.path.join(root_path, 'to','playlist'))
333 >>> p.info['note'] = 'An example playlist'
334 >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'one'))
335 >>> c.info['note'] = 'The first curve'
337 >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'two'))
338 >>> c.info['note'] = 'The second curve'
341 '\xa1\x1ax\xb1|\x84uA\xe4\x1d\xbf`\x004|\x82\xc2\xdd\xc1\x9e'
343 string = self.flatten()
344 return hashlib.sha1(string).digest()
347 """Create a string representation of the playlist.
349 A playlist is a YAML document with the following minimal syntax::
353 - path: picoforce.000
354 - path: picoforce.001
356 Relative paths are interpreted relative to the location of the
362 >>> from .engine import CommandMessage
364 >>> root_path = os.path.sep + 'path'
365 >>> p = FilePlaylist(drivers=[],
366 ... path=os.path.join(root_path, 'to','playlist'))
367 >>> p.info['note'] = 'An example playlist'
368 >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'one'))
369 >>> c.info['note'] = 'The first curve'
371 >>> c = curve.Curve(os.path.join(root_path, 'to', 'curve', 'two'))
372 >>> c.info['attr with spaces'] = 'The second curve\\nwith endlines'
373 >>> c.command_stack.extend([
374 ... CommandMessage('command A', {'arg 0':0, 'arg 1':'X'}),
375 ... CommandMessage('command B', {'arg 0':1, 'arg 1':'Y'}),
378 >>> print p.flatten() # doctest: +REPORT_UDIFF
379 # Hooke playlist version 0.2
383 _relative_curve_paths: true
385 info: {note: An example playlist}
387 - info: {note: The first curve}
390 - command_stack: !!python/object/new:hooke.command_stack.CommandStack
392 - !!python/object:hooke.engine.CommandMessage
393 arguments: {arg 0: 0, arg 1: X}
395 - !!python/object:hooke.engine.CommandMessage
396 arguments: {arg 0: 1, arg 1: Y}
398 info: {attr with spaces: 'The second curve
404 path: /path/to/playlist.hkp
407 >>> p._relative_curve_paths = False
408 >>> print p.flatten() # doctest: +REPORT_UDIFF
409 # Hooke playlist version 0.2
413 _relative_curve_paths: false
415 info: {note: An example playlist}
417 - info: {note: The first curve}
419 path: /path/to/curve/one
420 - command_stack: !!python/object/new:hooke.command_stack.CommandStack
422 - !!python/object:hooke.engine.CommandMessage
423 arguments: {arg 0: 0, arg 1: X}
425 - !!python/object:hooke.engine.CommandMessage
426 arguments: {arg 0: 1, arg 1: Y}
428 info: {attr with spaces: 'The second curve
432 path: /path/to/curve/two
434 path: /path/to/playlist.hkp
438 yaml_string = yaml.dump(self.__getstate__(), allow_unicode=True)
439 return ('# Hooke playlist version %s\n' % self.version) + yaml_string
441 def from_string(self, string):
442 u"""Load a playlist from a string.
449 >>> string = '''# Hooke playlist version 0.2
452 ... - path: picoforce.000
453 ... - path: picoforce.001
455 >>> p = FilePlaylist(drivers=[],
456 ... path=os.path.join('/path', 'to', 'my', 'playlist'))
457 >>> p.from_string(string)
460 /path/to/my/picoforce.000
461 /path/to/my/picoforce.001
463 More complicated example.
465 >>> string = '''# Hooke playlist version 0.2
466 ... _base_path: /path/to
470 ... _relative_curve_paths: true
471 ... info: {note: An example playlist}
473 ... - info: {note: The first curve}
475 ... - command_stack: !!python/object/new:hooke.command_stack.CommandStack
477 ... - !!python/object:hooke.engine.CommandMessage
478 ... arguments: {arg 0: 0, arg 1: X}
479 ... command: command A
480 ... - !!python/object:hooke.engine.CommandMessage
481 ... arguments: {arg 0: 1, arg 1: Y}
482 ... command: command B
483 ... info: {attr with spaces: 'The second curve
488 ... name: playlist.hkp
489 ... path: /path/to/playlist.hkp
492 >>> p = FilePlaylist(drivers=[],
493 ... path=os.path.join('path', 'to', 'my', 'playlist'))
494 >>> p.from_string(string)
498 {'note': 'An example playlist'}
500 ... print curve.name, curve.path
501 one /path/to/curve/one
502 two /path/to/curve/two
503 >>> p[-1].info['attr with spaces']
504 'The second curve\\nwith endlines'
505 >>> type(p[-1].command_stack)
506 <class 'hooke.command_stack.CommandStack'>
507 >>> p[-1].command_stack # doctest: +NORMALIZE_WHITESPACE
508 [<CommandMessage command A {arg 0: 0, arg 1: X}>,
509 <CommandMessage command B {arg 0: 1, arg 1: Y}>]
511 state = yaml.load(string)
512 self.__setstate__(state)
514 def save(self, path=None, makedirs=True):
515 """Saves the playlist to a YAML file.
518 dirname = os.path.dirname(self.path) or '.'
519 if makedirs == True and not os.path.isdir(dirname):
521 with open(self.path, 'w') as f:
522 f.write(self.flatten())
523 self._digest = self.digest()
525 def load(self, path=None, identify=True, hooke=None):
526 """Load a playlist from a file.
529 with open(self.path, 'r') as f:
531 self.from_string(text)
532 self._digest = self.digest()
534 curve.set_hooke(hooke)
536 curve.identify(self.drivers)
539 class Playlists (NoteIndexList):
540 """A :class:`NoteIndexList` of :class:`FilePlaylist`\s.
542 def __init__(self, *arg, **kwargs):
543 super(Playlists, self).__init__(*arg, **kwargs)
545 def _item_getstate(self, item):
546 assert isinstance(item, FilePlaylist), type(item)
547 return item.__getstate__()
549 def _item_setstate(self, state):
550 item = FilePlaylist(drivers=[])
551 item.__setstate__(state)