1
# Copyright 2006 Joe Wreschnig
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License version 2 as
5
# published by the Free Software Foundation
7
# $Id: _library.py 4029 2007-04-27 06:01:12Z piman $
9
"""Base library classes.
11
These classes are the most basic library classes. As such they are the
12
least useful but most content-agnostic.
15
import cPickle as pickle
24
# Windows doesn't have fcntl, just don't lock for now
30
class Library(gtk.Object):
31
"""A Library contains useful objects.
33
The only required method these objects support is a .key
34
attribute, but specific types of libraries may require more
38
SIG_PYOBJECT = (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (object,))
40
'changed': SIG_PYOBJECT,
41
'removed': SIG_PYOBJECT,
42
'added': SIG_PYOBJECT,
48
def __init__(self, name=None):
49
super(Library, self).__init__()
52
for key in ['get', 'keys', 'values', 'items', 'iterkeys',
53
'itervalues', 'iteritems', 'has_key']:
54
setattr(self, key, getattr(self._contents, key))
55
if self.librarian is not None and name is not None:
56
self.librarian.register(self, name)
59
"""Add items. This causes an 'added' signal.
61
Return the list of items actually added, filtering out items
62
already in the library.
64
items = filter(lambda item: item not in self, items)
66
self._contents[item.key] = item
67
self.emit('added', items)
70
def remove(self, items):
71
"""Remove items. This causes a 'removed' signal."""
73
del(self._contents[item.key])
74
self.emit('removed', items)
76
def changed(self, items):
77
"""Alert other users that these items have changed.
79
This causes a 'changed' signal. If a librarian is available
80
this function will call its changed method instead, and all
81
libraries that librarian manages may fire a 'changed' signal.
83
The item list may be filtered to those items actually in the
84
library. If a librarian is available, it will handle the
85
filtering instead. That means if this method is delegated to
86
the librarian, this library's changed signal may not fire, but
89
if self.librarian and self in self.librarian.libraries.itervalues():
90
self.librarian.changed(items)
92
items = filter(self.__contains__, items)
95
def _changed(self, items):
96
# Called by the changed method and Librarians.
97
self.emit('changed', items)
100
"""Iterate over the items in the library."""
101
return self._contents.itervalues()
104
"""The number of items in the library."""
105
return len(self._contents)
107
def __getitem__(self, key):
108
"""Find a item given its key."""
109
return self._contents[key]
111
def __contains__(self, item):
112
"""Check if a key or item is in the library."""
113
try: return item in self._contents or item.key in self._contents
114
except AttributeError: return False
116
def load(self, filename, skip=False):
117
"""Load a library from a file, containing a picked list.
119
Loading does not cause added, changed, or removed signals.
122
if os.path.exists(filename):
123
fileobj = file(filename, "rb")
124
try: items = pickle.load(fileobj)
125
except (pickle.PickleError, EnvironmentError):
126
traceback.print_exc()
127
try: shutil.copy(filename, filename + ".not-valid")
128
except EnvironmentError:
129
traceback.print_exc()
133
except EnvironmentError:
137
for item in filter(skip, items):
138
self._contents[item.key] = item
140
map(self._load, items)
142
def _load(self, item):
143
"""Load a item. Return (changed, removed)."""
144
# Subclases should override this if they want to check
145
# item validity; see FileLibrary.
146
self._contents[item.key] = item
148
def save(self, filename):
149
"""Save the library to the given filename."""
150
if not os.path.isdir(os.path.dirname(filename)):
151
os.makedirs(os.path.dirname(filename))
152
fileobj = file(filename + ".tmp", "wb")
153
if fcntl is not None:
154
fcntl.flock(fileobj.fileno(), fcntl.LOCK_EX)
155
items = self.values()
156
for masked in self._masked.values():
157
items.extend(masked.values())
158
# Item keys are often based on filenames, in which case
159
# sorting takes advantage of the filesystem cache when we
160
# reload/rescan the files.
161
items.sort(key=lambda item: item.key)
162
pickle.dump(items, fileobj, pickle.HIGHEST_PROTOCOL)
163
os.rename(filename + ".tmp", filename)
166
class Librarian(gtk.Object):
167
"""The librarian is a nice interface to all active libraries.
169
Librarians are a kind of meta-library. When any of their
170
registered libraries fire a signal, they fire the same
171
signal. Likewise, they provide various methods equivalent to the
172
ones found in libraries that group the results of the real
176
libraries -- a dict mapping library names to libraries
179
SIG_PYOBJECT = (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (object,))
181
'changed': SIG_PYOBJECT,
182
'removed': SIG_PYOBJECT,
183
'added': SIG_PYOBJECT,
187
super(Librarian, self).__init__()
191
def register(self, library, name):
192
"""Register a library with this librarian."""
193
if name in self.libraries or name in self.__signals:
194
raise ValueError("library %r is already active" % name)
196
added_sig = library.connect('added', self.__added)
197
removed_sig = library.connect('removed', self.__removed)
198
changed_sig = library.connect('changed', self.__changed)
199
library.connect('destroy', self.__unregister, name)
200
self.libraries[name] = library
201
self.__signals[library] = [added_sig, removed_sig, changed_sig]
203
def __unregister(self, library, name):
204
# This function, unlike register, should be private.
205
# Libraries get unregistered at the discretion of the
206
# librarian, not the libraries.
207
del(self.libraries[name])
208
del(self.__signals[library])
210
# FIXME: We can be smarter about this -- queue a list of items
211
# and fire the signal after a short wait, to take advantage of
212
# a case where many libraries fire a signal at the same time (or
213
# one fires a signal often).
215
def __changed(self, library, items):
216
self.emit('changed', items)
218
def __added(self, library, items):
219
self.emit('added', items)
221
def __removed(self, library, items):
222
self.emit('removed', items)
224
def changed(self, items):
225
"""Triage the items and inform their real libraries."""
226
for library in self.libraries.itervalues():
227
in_library = filter(library.__contains__, items)
229
library._changed(in_library)
231
def __getitem__(self, key):
232
"""Find a item given its key."""
233
for library in self.libraries.itervalues():
234
try: return library[key]
235
except KeyError: pass
236
else: raise KeyError, key
238
def get(self, key, default=None):
239
try: return self[key]
240
except KeyError: return default
242
def remove(self, items):
243
"""Remove items from all libraries."""
244
for library in self.libraries.itervalues():
245
library.remove(filter(library.__contains__, items))
247
def __contains__(self, item):
248
"""Check if a key or item is in the library."""
249
for library in self.libraries.itervalues():
255
"""Iterate over all items in all libraries."""
256
return itertools.chain(*self.libraries.itervalues())
258
def move(self, items, from_, to):
259
"""Move items from one library to another.
261
This causes 'removed' signals on the from library, and 'added'
262
signals on the 'to' library, but will not cause any signals
263
to be emitted via this librarian.
266
from_.handler_block(self.__signals[from_][1])
267
to.handler_block(self.__signals[to][0])
271
from_.handler_unblock(self.__signals[from_][1])
272
to.handler_unblock(self.__signals[to][0])