Merge lp:~kamstrup/zeitgeist/blacklist into lp:zeitgeist/0.1
- blacklist
- Merge into 0.8-python
Status: | Merged |
---|---|
Merged at revision: | not available |
Proposed branch: | lp:~kamstrup/zeitgeist/blacklist |
Merge into: | lp:zeitgeist/0.1 |
Diff against target: |
366 lines (+236/-10) 9 files modified
_zeitgeist/engine/Makefile.am (+2/-0) _zeitgeist/engine/__init__.py (+3/-1) _zeitgeist/engine/extension.py (+47/-6) _zeitgeist/engine/extensions/Makefile.am (+5/-0) _zeitgeist/engine/extensions/blacklist.py (+100/-0) _zeitgeist/engine/resonance_engine.py (+6/-3) configure.ac (+1/-0) test/blacklist-test.py (+65/-0) test/resonance-engine-test.py (+7/-0) |
To merge this branch: | bzr merge lp:~kamstrup/zeitgeist/blacklist |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Siegfried Gevatter | Approve | ||
Markus Korn | diff | Needs Fixing | |
Review via email: mp+16654@code.launchpad.net |
Commit message
Description of the change
Mikkel Kamstrup Erlandsen (kamstrup) wrote : | # |
- 1244. By Mikkel Kamstrup Erlandsen
-
Add a test to the blacklist extension to check that events are indeed filtered/let through correctly
Markus Korn (thekorn) wrote : | # |
Great to have this blacklist framework in a mergable state, however some first comments (I did not run the code yet)
* docstring in load_class(): we are working on zeitgeist and not on zeitgesit ;)
* statement to unload() all extension: list(self.
* the popo code made me smile ;) but it is somehow not looking right,
** creating a list containing three None objects is looking strange, but ok. I think
popo = list()
popo.
[...]
is looking much nicer
** the for loop looks wrong, I think the body must popo[1][i] = map(unicode, subj)
I think something like this is more readable:
>>> popo = list()
>>> popo.append(
>>> popo.append(
>>> popo.append(
Mikkel Kamstrup Erlandsen (kamstrup) wrote : | # |
> Great to have this blacklist framework in a mergable state, however some first
> comments (I did not run the code yet)
>
> * docstring in load_class(): we are working on zeitgeist and not on zeitgesit
Fixed
> * statement to unload() all extension: list(self.
> looking strange, why not self.__
This is to avoid a concurrent modification of the key set while we are iterating over it - maybe there is a nicer way..?
> * the popo code made me smile ;) but it is somehow not looking right,
> ** creating a list containing three None objects is looking strange, but ok.
> I think
> popo = list()
> popo.append(
> [...]
> is looking much nicer
> ** the for loop looks wrong, I think the body must popo[1][i] =
> map(unicode, subj)
>
> I think something like this is more readable:
>
> >>> popo = list()
> >>> popo.append(
> >>> popo.append(
> >>> popo.append(
I changed it to use your proposal here. I think you are also right there was a bug in the for loop. I'll add an extra unit test for this later.
Thanks for the review so far
- 1245. By Mikkel Kamstrup Erlandsen
-
Updates reflecting Markus' comments on LP review
- 1246. By Mikkel Kamstrup Erlandsen
-
Add needed autofoo magic to include the new blacklist extension files in the distribution tarball
Siegfried Gevatter (rainct) wrote : | # |
Committed.
-------
revision <email address hidden> (1251)
Author: Siegfried-Angel Gevatter Pujals <email address hidden>
Date: Mon 2010-01-04 20:10:40 +0100
Branch: zeitgeist-trunk
Bugs: https:/
Merge with lp:~kamstrup/zeitgeist/blacklist
- Add Mikkel Kamstrup's implementation of a blacklist,
together with the needed changes to the extensions system.
Good work Mikkel!
Some changes from my side:
- Add a .constants namespace to __init__.py and move several
constants into there. They can be used both by Zeitgeist
itself and by extensions (eg. DATA_PATH).
- Fix test/engine-
now that there are extensions loaded by default.
- Some other little tweaks.
-------
I have some concerns but we can discuss further changes as bugs.
Good work!
Preview Diff
1 | === modified file '_zeitgeist/engine/Makefile.am' |
2 | --- _zeitgeist/engine/Makefile.am 2009-11-30 23:22:36 +0000 |
3 | +++ _zeitgeist/engine/Makefile.am 2009-12-31 09:11:14 +0000 |
4 | @@ -1,3 +1,5 @@ |
5 | +SUBDIRS = extensions |
6 | + |
7 | appdir = $(prefix)/share/zeitgeist/_zeitgeist/engine/ |
8 | |
9 | app_PYTHON = \ |
10 | |
11 | === modified file '_zeitgeist/engine/__init__.py' |
12 | --- _zeitgeist/engine/__init__.py 2009-11-27 13:11:52 +0000 |
13 | +++ _zeitgeist/engine/__init__.py 2009-12-31 09:11:14 +0000 |
14 | @@ -32,6 +32,8 @@ |
15 | AVAILABLE_ENGINES = ["resonance"] |
16 | ENGINE_FALLBACK = AVAILABLE_ENGINES[0] |
17 | |
18 | +DEFAULT_EXTENSIONS = ["_zeitgeist.engine.extensions.blacklist.Blacklist"] |
19 | + |
20 | def get_engine_type(): |
21 | """ Returns the value of the $ZEITGEIST_ENGINE environment variable or, |
22 | if it isn't defined, the default engine.""" |
23 | @@ -60,7 +62,7 @@ |
24 | |
25 | # See if we can reuse _engine |
26 | if _engine and not _engine.is_closed(): |
27 | - running_type = _engine.__module__.split(".").pop().lower() |
28 | + running_type = _engine.__module__.split(".").pop().lower().rpartition("_")[0] |
29 | if not running_type == engine_type: |
30 | raise RuntimeError( |
31 | ("There is already a zeitgeist engine running. But this " |
32 | |
33 | === modified file '_zeitgeist/engine/extension.py' |
34 | --- _zeitgeist/engine/extension.py 2009-12-30 19:03:24 +0000 |
35 | +++ _zeitgeist/engine/extension.py 2009-12-31 09:11:14 +0000 |
36 | @@ -17,6 +17,10 @@ |
37 | # You should have received a copy of the GNU Lesser General Public License |
38 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
39 | |
40 | +import logging |
41 | +logging.basicConfig(level=logging.DEBUG) |
42 | +log = logging.getLogger("zeitgeist.extension") |
43 | + |
44 | class Extension(object): |
45 | """ Base class for all extensions |
46 | |
47 | @@ -72,7 +76,29 @@ |
48 | should see it |
49 | """ |
50 | return event |
51 | - |
52 | + |
53 | +def load_class(path): |
54 | + """ |
55 | + Load and return a class from a fully qualified string. |
56 | + Fx. "_zeitgeist.engine.extensions.myext.MyClass" |
57 | + """ |
58 | + module, dot, cls_name = path.rpartition(".") |
59 | + parts = module.split(".") |
60 | + module = __import__(module) |
61 | + for part in parts[1:]: |
62 | + try: |
63 | + module = getattr(module, part) |
64 | + except AttributeError: |
65 | + raise ImportError( |
66 | + "No such submodule '%s' when loading %s" % (part, path)) |
67 | + |
68 | + try: |
69 | + cls = getattr(module, cls_name) |
70 | + except AttributeError: |
71 | + raise ImportError("No such class '%s' in module %s" % (cls_name,path)) |
72 | + |
73 | + return cls |
74 | + |
75 | class ExtensionsCollection(object): |
76 | """ Collection to manage all extensions """ |
77 | |
78 | @@ -88,6 +114,7 @@ |
79 | return "%s(%r)" %(self.__class__.__name__, sorted(self.__methods.keys())) |
80 | |
81 | def load(self, extension): |
82 | + log.debug("Loading extensions '%s'" % extension.__name__) |
83 | if not issubclass(extension, Extension): |
84 | raise TypeError( |
85 | "Unable to load %r, all extensions have to be subclasses of %r" %(extension, Extension) |
86 | @@ -99,11 +126,25 @@ |
87 | self._register_method(method, getattr(obj, method)) |
88 | self.__extensions[obj.__class__.__name__] = obj |
89 | |
90 | - def unload(self, extension): |
91 | - obj = self.__extensions[extension.__name__] |
92 | - for method in obj.PUBLIC_METHODS: |
93 | - del self.methods[method] |
94 | - del self.__extensions[extension.__name__] |
95 | + def unload(self, extension=None): |
96 | + """ |
97 | + Unload a specified extension or unload all extensions if |
98 | + no extension is given |
99 | + """ |
100 | + if extension is None: |
101 | + log.debug("Unloading all extensions") |
102 | + |
103 | + # We need to clone the key list to avoid concurrent |
104 | + # modification of the extension dict |
105 | + for ext_name in list(self.__extensions.iterkeys()): |
106 | + self.unload(self.__extensions[ext_name]) |
107 | + else: |
108 | + log.debug("Unloading extension '%s'" \ |
109 | + % extension.__class__.__name__) |
110 | + obj = self.__extensions[extension.__class__.__name__] |
111 | + for method in obj.PUBLIC_METHODS: |
112 | + del self.methods[method] |
113 | + del self.__extensions[extension.__class__.__name__] |
114 | |
115 | def apply_get_hooks(self, event): |
116 | # Apply extension filters if we have an event |
117 | |
118 | === added directory '_zeitgeist/engine/extensions' |
119 | === added file '_zeitgeist/engine/extensions/Makefile.am' |
120 | --- _zeitgeist/engine/extensions/Makefile.am 1970-01-01 00:00:00 +0000 |
121 | +++ _zeitgeist/engine/extensions/Makefile.am 2009-12-31 09:11:14 +0000 |
122 | @@ -0,0 +1,5 @@ |
123 | +appdir = $(prefix)/share/zeitgeist/_zeitgeist/engine/extensions |
124 | + |
125 | +app_PYTHON = \ |
126 | + __init__.py \ |
127 | + blacklist.py |
128 | |
129 | === added file '_zeitgeist/engine/extensions/blacklist.py' |
130 | --- _zeitgeist/engine/extensions/blacklist.py 1970-01-01 00:00:00 +0000 |
131 | +++ _zeitgeist/engine/extensions/blacklist.py 2009-12-31 09:11:14 +0000 |
132 | @@ -0,0 +1,100 @@ |
133 | +# -.- coding: utf-8 -.- |
134 | + |
135 | +# Zeitgeist |
136 | +# |
137 | +# Copyright © 2009 Mikkel Kamstrup Erlandsen <mikkel.kamstrup@gmail.com> |
138 | +# |
139 | +# This program is free software: you can redistribute it and/or modify |
140 | +# it under the terms of the GNU Lesser General Public License as published by |
141 | +# the Free Software Foundation, either version 3 of the License, or |
142 | +# (at your option) any later version. |
143 | +# |
144 | +# This program is distributed in the hope that it will be useful, |
145 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
146 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
147 | +# GNU Lesser General Public License for more details. |
148 | +# |
149 | +# You should have received a copy of the GNU Lesser General Public License |
150 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
151 | + |
152 | +import os |
153 | +import pickle |
154 | +import dbus |
155 | +import dbus.service |
156 | +from xdg import BaseDirectory |
157 | +from zeitgeist.datamodel import Event |
158 | +from _zeitgeist.engine.extension import Extension |
159 | + |
160 | +import logging |
161 | +logging.basicConfig(level=logging.DEBUG) |
162 | +log = logging.getLogger("zeitgeist.blacklist") |
163 | + |
164 | +CONFIG_FILE = os.path.join(BaseDirectory.save_config_path("zeitgeist"), |
165 | + "blacklist.pickle") |
166 | + |
167 | +DBUS_OBJECT_PATH = "/org/gnome/zeitgeist/blacklist" |
168 | +DBUS_INTERFACE = "org.gnome.zeitgeist.Blacklist" |
169 | +SIG_EVENT="asaasay" |
170 | + |
171 | +def _event2popo(ev): |
172 | + """ |
173 | + Ensure that an Event instance is a Plain Old Python Object (popo) |
174 | + without DBus wrappings etc. |
175 | + """ |
176 | + popo = list() |
177 | + popo.append(map(unicode, ev[0])) |
178 | + popo.append([map(unicode, subj) for subj in ev[1]]) |
179 | + popo.append(str(ev[2])) |
180 | + return popo |
181 | + |
182 | +class Blacklist(Extension, dbus.service.Object): |
183 | + PUBLIC_METHODS = ["set_blacklist", "get_blacklist"] |
184 | + |
185 | + def __init__ (self, engine): |
186 | + Extension.__init__(self, engine) |
187 | + dbus.service.Object.__init__(self, dbus.SessionBus(), |
188 | + DBUS_OBJECT_PATH) |
189 | + if os.path.exists(CONFIG_FILE): |
190 | + try: |
191 | + raw_blacklist = pickle.load(file(CONFIG_FILE)) |
192 | + self._blacklist = map(Event, raw_blacklist) |
193 | + log.debug("Loaded blacklist config from %s" |
194 | + % CONFIG_FILE) |
195 | + except Exception, e: |
196 | + log.warn("Failed to load blacklist config file %s: %s"\ |
197 | + % (CONFIG_FILE, e)) |
198 | + self._blacklist = [] |
199 | + else: |
200 | + log.debug("No existing blacklist config found") |
201 | + self._blacklist = [] |
202 | + |
203 | + def insert_event_hook(self, event): |
204 | + for tmpl in self._blacklist: |
205 | + if event.matches_template(tmpl) : return None |
206 | + return event |
207 | + |
208 | + # PUBLIC |
209 | + def set_blacklist(self, event_templates): |
210 | + self._blacklist = event_templates |
211 | + map(Event._make_dbus_sendable, self._blacklist) |
212 | + |
213 | + out = file(CONFIG_FILE, "w") |
214 | + pickle.dump(map(_event2popo, self._blacklist), out) |
215 | + out.close() |
216 | + log.debug("Blacklist updated: %s" % self._blacklist) |
217 | + |
218 | + # PUBLIC |
219 | + def get_blacklist(self): |
220 | + return self._blacklist |
221 | + |
222 | + @dbus.service.method(DBUS_INTERFACE, |
223 | + in_signature="a("+SIG_EVENT+")") |
224 | + def SetBlacklist(self, event_templates): |
225 | + tmp = map(Event, event_templates) |
226 | + self.set_blacklist(tmp) |
227 | + |
228 | + @dbus.service.method(DBUS_INTERFACE, |
229 | + in_signature="", out_signature="a("+SIG_EVENT+")") |
230 | + def GetBlacklist(self): |
231 | + return self.get_blacklist() |
232 | + |
233 | |
234 | === modified file '_zeitgeist/engine/resonance_engine.py' |
235 | --- _zeitgeist/engine/resonance_engine.py 2009-12-30 18:24:15 +0000 |
236 | +++ _zeitgeist/engine/resonance_engine.py 2009-12-31 09:11:14 +0000 |
237 | @@ -28,7 +28,7 @@ |
238 | import gettext |
239 | import logging |
240 | |
241 | -from extension import ExtensionsCollection |
242 | +from extension import ExtensionsCollection, load_class |
243 | |
244 | from zeitgeist.datamodel import Subject, Event, StorageState, TimeRange, \ |
245 | ResultType, get_timestamp_for_now |
246 | @@ -290,8 +290,10 @@ |
247 | raise RuntimeError("old database version") |
248 | |
249 | # Load extensions |
250 | - # Right now we don't load any default extension |
251 | - self.__extensions = ExtensionsCollection(self) |
252 | + default_extensions = \ |
253 | + map(load_class, _zeitgeist.engine.DEFAULT_EXTENSIONS) |
254 | + self.__extensions = ExtensionsCollection( |
255 | + self, defaults=default_extensions) |
256 | |
257 | self._interpretation = TableLookup(cursor, "interpretation") |
258 | self._manifestation = TableLookup(cursor, "manifestation") |
259 | @@ -304,6 +306,7 @@ |
260 | |
261 | def close(self): |
262 | global _cursor |
263 | + self.extensions.unload() |
264 | self._cursor.connection.close() |
265 | self._cursor = _cursor = None |
266 | |
267 | |
268 | === modified file 'configure.ac' |
269 | --- configure.ac 2009-11-27 20:32:54 +0000 |
270 | +++ configure.ac 2009-12-31 09:11:14 +0000 |
271 | @@ -23,5 +23,6 @@ |
272 | _zeitgeist/loggers/Makefile |
273 | _zeitgeist/loggers/datasources/Makefile |
274 | _zeitgeist/engine/Makefile |
275 | + _zeitgeist/engine/extensions/Makefile |
276 | ]) |
277 | AC_OUTPUT |
278 | |
279 | === added file 'test/blacklist-test.py' |
280 | --- test/blacklist-test.py 1970-01-01 00:00:00 +0000 |
281 | +++ test/blacklist-test.py 2009-12-31 09:11:14 +0000 |
282 | @@ -0,0 +1,65 @@ |
283 | +#!/usr/bin/python |
284 | +# -.- coding: utf-8 -.- |
285 | + |
286 | +# Update python path to use local zeitgeist module |
287 | +import sys |
288 | +import os |
289 | +import unittest |
290 | +import dbus |
291 | + |
292 | +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) |
293 | +from zeitgeist.datamodel import * |
294 | +from testutils import RemoteTestCase |
295 | + |
296 | +class BlacklistTest(RemoteTestCase): |
297 | + |
298 | + def __init__(self, methodName): |
299 | + super(BlacklistTest, self).__init__(methodName) |
300 | + self.blacklist = None |
301 | + |
302 | + def setUp(self): |
303 | + # We set up the connection lazily in order to wait for the |
304 | + # engine to come up |
305 | + super(BlacklistTest, self).setUp() |
306 | + obj = dbus.SessionBus().get_object("org.gnome.zeitgeist.Engine", |
307 | + "/org/gnome/zeitgeist/blacklist") |
308 | + self.blacklist = dbus.Interface(obj, "org.gnome.zeitgeist.Blacklist") |
309 | + |
310 | + def testClear(self): |
311 | + self.blacklist.SetBlacklist([]) |
312 | + empty = self.blacklist.GetBlacklist() |
313 | + self.assertEquals(empty, []) |
314 | + |
315 | + def testSetOne(self): |
316 | + orig = Event.new_for_values(interpretation=Interpretation.OPEN_EVENT, |
317 | + subject_uri="http://nothingtoseehere.gov") |
318 | + self.blacklist.SetBlacklist([orig]) |
319 | + result = map(Event, self.blacklist.GetBlacklist()) |
320 | + |
321 | + self.assertEquals(len(result), 1) |
322 | + result = result[0] |
323 | + self.assertEquals(result.manifestation, "") |
324 | + self.assertEquals(result.interpretation, Interpretation.OPEN_EVENT) |
325 | + self.assertEquals(len(result.subjects), 1) |
326 | + self.assertEquals(result.subjects[0].uri, "http://nothingtoseehere.gov") |
327 | + self.assertEquals(result.subjects[0].interpretation, "") |
328 | + |
329 | + def testApplyBlacklist(self): |
330 | + self.testSetOne() |
331 | + ev = Event() |
332 | + ev.interpretation = Interpretation.OPEN_EVENT |
333 | + ev.manifestation = Manifestation.USER_ACTIVITY |
334 | + ev.actor = "app.//foo.desktop" |
335 | + subj = ev.append_subject() |
336 | + subj.uri = "http://nothingtoseehere.gov" |
337 | + inserted_ids = self.insertEventsAndWait([ev]) |
338 | + self.assertEquals(1, len(inserted_ids)) |
339 | + self.assertEquals(0, inserted_ids[0]) |
340 | + |
341 | + # Now change the event to pass the blacklist |
342 | + subj.uri = "htpp://totallyvaliduri.com" |
343 | + inserted_ids = self.insertEventsAndWait([ev]) |
344 | + self.assertEquals(1, len(inserted_ids)) |
345 | + self.assertTrue(0 != inserted_ids[0]) |
346 | +if __name__ == "__main__": |
347 | + unittest.main() |
348 | |
349 | === modified file 'test/resonance-engine-test.py' |
350 | --- test/resonance-engine-test.py 2009-12-30 17:34:24 +0000 |
351 | +++ test/resonance-engine-test.py 2009-12-31 09:11:14 +0000 |
352 | @@ -39,7 +39,14 @@ |
353 | def setUp (self): |
354 | global test_event_1 |
355 | test_event_1 = create_test_event_1() |
356 | + |
357 | + # Some extensions keep state around that interferes |
358 | + # with the tests, so we disable all extensions |
359 | + _zeitgeist.engine.DEFAULT_EXTENSIONS = [] |
360 | + |
361 | + # Memory backed tmp DB |
362 | _zeitgeist.engine.DB_PATH = ":memory:" |
363 | + |
364 | self.engine = create_engine() |
365 | |
366 | def tearDown (self): |
I think the blacklisting framework is good to go in now. Some thing worth mentioning:
* I added a DEFAULT_EXTENSIONS var in _zeitgeist.engine with a list of class names to load by default. This names are string encoded in order to be able to pull them from an environment var in the future
* I use a pickled format to store the blacklist in and not a human readable format like fx. JSON, for a reason. Before we completely seal off the API I don't want an on disk format people can tinker with
* I disable all extensions in the resonance unit tests because extensions can hold state we can not reset a priori (extensions can do anything mind you). Fx. there seems to be a bug in Python-DBus making it impossible to unexport the DBus object the Blacklist extension exports...
* I probably forgot something I should have mentioned here. Forgive me :-)