Merge lp:~kamstrup/zeitgeist/blacklist into lp:zeitgeist/0.1

Proposed by Mikkel Kamstrup Erlandsen
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
Reviewer Review Type Date Requested Status
Siegfried Gevatter Approve
Markus Korn diff Needs Fixing
Review via email: mp+16654@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Mikkel Kamstrup Erlandsen (kamstrup) wrote :

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 :-)

lp:~kamstrup/zeitgeist/blacklist updated
1244. By Mikkel Kamstrup Erlandsen

Add a test to the blacklist extension to check that events are indeed filtered/let through correctly

Revision history for this message
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.__extensions.iterkeys()) is looking strange, why not self.__extensions.keys() ?
* 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(map(unicode, ev[0]))
  [...]
     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(map(unicode, ev[0]))
>>> popo.append([map(unicode, subj) for subj in ev[1]])
>>> popo.append(str(ev[2]))

review: Needs Fixing (diff)
Revision history for this message
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.__extensions.iterkeys()) is
> looking strange, why not self.__extensions.keys() ?

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(map(unicode, ev[0]))
> [...]
> 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(map(unicode, ev[0]))
> >>> popo.append([map(unicode, subj) for subj in ev[1]])
> >>> popo.append(str(ev[2]))

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

lp:~kamstrup/zeitgeist/blacklist updated
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

Revision history for this message
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://launchpad.net/bugs/447417 fixed

    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-extension-test.py so that it doesn't break
       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!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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):