Merge lp:~robru/gwibber/dbusmenu into lp:~barry/gwibber/py3

Proposed by Robert Bruce Park
Status: Merged
Merged at revision: 1405
Proposed branch: lp:~robru/gwibber/dbusmenu
Merge into: lp:~barry/gwibber/py3
Diff against target: 363 lines (+159/-123)
4 files modified
gwibber/gwibber/main.py (+8/-0)
gwibber/gwibber/tests/test_menu.py (+52/-0)
gwibber/gwibber/utils/menus.py (+99/-0)
gwibber/microblog/dispatcher.py (+0/-123)
To merge this branch: bzr merge lp:~robru/gwibber/dbusmenu
Reviewer Review Type Date Requested Status
Barry Warsaw Pending
Review via email: mp+122139@code.launchpad.net

Description of the change

This is the MenuManager that handles all of the Dbusmenu code. It also contains a stub of MessagingMenu code, but that isn't implemented yet.

The test coverage is pretty weak, I couldn't figure out how to test it 'properly' within the context of the larger test suite, but I made it possible so that you can run the file independently of the rest of the codebase to ensure that it actually is working.

I've also modified main.py in order to invoke it, but that work is a bit incomplete because it needs a couple callbacks into the Dispatcher, which doesn't exist yet.

Please let me know if there's a way to improve the test, and I'll do it. Otherwise it's ready for merging I think.

To post a comment you must log in.
Revision history for this message
Barry Warsaw (barry) wrote :
Download full text (6.7 KiB)

Thank you for yet another nice branch nailing yet another nail in the
dispatcher coffin! I'll provide some specific comments about the code inline,
though I'll ignore all the nice deletions from dispatcher.py.

I think you're only hope for a *unit* test is to mock .launcher and check that
the right calls have been made when update_unread_count() is called. That
should be an easy test to add, and I think you should do it.

What you have in the __main__ is essentially an integration test. One way to
add a test for this is to add a dbus call to the TestService in
gwibber/testing/service.py which would take an integer and turn around and
call update_unread_count() with that value. The problem is, I can't think of
a good way to verify that the test is working correctly. Is it possible to
query the menus to see if they've been changed, and if so in what way?

We really don't want an automated test that requires any user interaction, and
without the ability to query the menus, it seems like that's all we're left
with. Maybe ask on the ubuntu-devel mailing list and see how other folks who
are writing similar dbusmenu applications do their testing. I just don't know
enough about this stuff to give good suggestions.

The only other suggestion I can give is to provide a README with instructions
for how to verify this is working to a Q/A person, or another developer.

Let's look at the code! I'll remove the diff hunks that look fine.

=== modified file 'gwibber/gwibber/main.py'
--- gwibber/gwibber/main.py 2012-08-21 22:42:24 +0000
+++ gwibber/gwibber/main.py 2012-08-30 20:10:23 +0000
> @@ -46,6 +47,16 @@
> DEFAULT_SQLITE_FILE = os.path.join(
> BaseDirectory.xdg_config_home, 'gwibber', 'gwibber.sqlite')
>
> +def refresh_callback(*ignore):
> + # TODO: Once the new Dispatcher is born, this method should trigger
> + # it to do a refresh.
> + pass
> +
> +def shutdown_callback(*ignore):
> + # TODO: Once the new Dispatcher is born, this method should trigger
> + # it to shutdown all of gwibber-*.
> + pass
> +

Do you expect these methods to move, or will they live permanently in main.py?

>
> def main():
> global log

=== added file 'gwibber/gwibber/utils/menus.py'
--- gwibber/gwibber/utils/menus.py 1970-01-01 00:00:00 +0000
+++ gwibber/gwibber/utils/menus.py 2012-08-30 20:10:23 +0000
> @@ -0,0 +1,97 @@
> +# Copyright (C) 2012 Canonical Ltd
> +#
> +# This program is free software: you can redistribute it and/or modify
> +# it under the terms of the GNU General Public License version 2 as
> +# published by the Free Software Foundation.
> +#
> +# This program is distributed in the hope that it will be useful,
> +# but WITHOUT ANY WARRANTY; without even the implied warranty of
> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
> +# GNU General Public License for more details.
> +#
> +# You should have received a copy of the GNU General Public License
> +# along with this program. If not, see <http://www.gnu.org/licenses/>.
> +
> +"""This file defines the Dbusmenu behavior used in Unity."""
> +
> +import subprocess
> +
> +from gettext import gettext as _
> +
> +try:
> + from gi.repository import Unity, D...

Read more...

Revision history for this message
Robert Bruce Park (robru) wrote :
Download full text (3.2 KiB)

On 12-08-30 04:05 PM, Barry Warsaw wrote:>> +def refresh_callback(*ignore):
 >> + # TODO: Once the new Dispatcher is born, this method should trigger
 >> + # it to do a refresh.
 >> + pass
 >> +
 >> +def shutdown_callback(*ignore):
 >> + # TODO: Once the new Dispatcher is born, this method should trigger
 >> + # it to shutdown all of gwibber-*.
 >> + pass
 >> +
 >
 > Do you expect these methods to move, or will they live permanently in
main.py?

I knew you wouldn't like these ;-)

They are not expected to do anything. I expect to delete them once we
have a working Dispatcher, and then we can just pass the appropriate
dispatcher methods directly into the MenuManager constructor. The only
reason that I did two separate ones instead of passing in the same one
twice, was that I thought that perhaps the shutdown callback may not
even need the dispatcher at all, if our dispatcher is simple enough.
perhaps shutdown_callback could be replaced with a lambda that just
calls mainloop.quit() or something. I'm not really sure, I was hoping
you'd be able to integrate this better.

 >> + def __init__(self, refresh_callback, shutdown_callback):
 >> + self.refresh = refresh_callback
 >> + self.shutdown = shutdown_callback
 >
 > Are these attributes useful (even potentially) outside of the class?
  If not,
 > it's probably better to prefix them with a single underscore.

They won't be, because anything that needs them will have equal access
to them from the place that they're passed in from (eg, if something
needs them, they can get it directly from main.py rather than from
MenuManager).

 >> + post_menu = Dbusmenu.Menuitem.new()
 >> + post_menu.property_set(Dbusmenu.MENUITEM_PROP_LABEL,
_('Update Status'))
 >> + post_menu.connect('item-activated', lambda *ignore:
 >> + subprocess.Popen('gwibber-poster', shell=False))
 >
 > lambda like this are pretty nasty actually. Also, I wonder about
connecting
 > this menu item to the Popen object resulting from this. The
subprocess may
 > not get called properly, or the child may not get properly waited on.
 >
 > What probably makes sense is to move this into a method which calls
 > subprocess.check_output() to ensure the subcommand is run. It'll have to
 > catch CalledProcessError in case the subprocess fails, and everything
should
 > be logged (i.e. any output and any non-zero return code).
 >
 > For example, what happens if gwibber-poster or gwibber-preferences
doesn't
 > exist on the system? We definitely don't want these menu items failing
 > silently, but there's probably not much other than logging you can do.

I hate to say this barry, but I'm not very familiar with either
subprocess, OR with the logger, but you seem to have strong opinions
about both. Is there any way that you could clean this part up to your
standards?

I agree that silent lambdas launching processes and ignoring output is
ugly, but I just don't know any other way. In my defence, this is almost
identical to what it was doing before, except instead of lambdas it
wasted a bunch of space defining instance methods, that identically
ignored their argument...

Read more...

lp:~robru/gwibber/dbusmenu updated
1406. By Robert Bruce Park

Add a basic unit test for the dbusmenu.

1407. By Robert Bruce Park

Private refresh and shutdown callbacks.

Revision history for this message
Barry Warsaw (barry) wrote :

On Aug 30, 2012, at 09:28 PM, Robert Bruce Park wrote:

>I hate to say this barry, but I'm not very familiar with either
>subprocess, OR with the logger, but you seem to have strong opinions
>about both. Is there any way that you could clean this part up to your
>standards?

No worries, I'll clean this up when I merge the branch.

-Barry

lp:~robru/gwibber/dbusmenu updated
1408. By Robert Bruce Park

Reduce ugliness in main.py.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'gwibber/gwibber/main.py'
2--- gwibber/gwibber/main.py 2012-08-21 22:42:24 +0000
3+++ gwibber/gwibber/main.py 2012-08-30 21:45:21 +0000
4@@ -37,6 +37,7 @@
5 from gwibber.service.streams import StreamManager
6 from gwibber.testing.service import TestService
7 from gwibber.utils.logging import initialize
8+from gwibber.utils.menus import MenuManager
9 from gwibber.utils.options import Options
10
11
12@@ -47,6 +48,12 @@
13 BaseDirectory.xdg_config_home, 'gwibber', 'gwibber.sqlite')
14
15
16+class DispatcherStub:
17+ # TODO: Once we have a real dispatcher, this can go away.
18+ def refresh(self):
19+ pass
20+
21+
22 def main():
23 global log
24 # Initialize command line options.
25@@ -70,6 +77,7 @@
26 # about unused local variables.
27 services.extend([
28 ConnectionMonitor(),
29+ MenuManager(DispatcherStub().refresh, lambda *args: loop.quit())
30 MessageManager(database),
31 SearchManager(database),
32 StreamManager(database),
33
34=== added file 'gwibber/gwibber/tests/test_menu.py'
35--- gwibber/gwibber/tests/test_menu.py 1970-01-01 00:00:00 +0000
36+++ gwibber/gwibber/tests/test_menu.py 2012-08-30 21:45:21 +0000
37@@ -0,0 +1,52 @@
38+# Copyright (C) 2012 Canonical Ltd
39+#
40+# This program is free software: you can redistribute it and/or modify
41+# it under the terms of the GNU General Public License version 2 as
42+# published by the Free Software Foundation.
43+#
44+# This program is distributed in the hope that it will be useful,
45+# but WITHOUT ANY WARRANTY; without even the implied warranty of
46+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
47+# GNU General Public License for more details.
48+#
49+# You should have received a copy of the GNU General Public License
50+# along with this program. If not, see <http://www.gnu.org/licenses/>.
51+
52+"""Test the MenuManager class."""
53+
54+__all__ = [
55+ 'TestMenu',
56+ ]
57+
58+
59+import unittest
60+
61+from gwibber.utils.menus import MenuManager
62+
63+try:
64+ # Python 3.3
65+ from unittest import mock
66+except ImportError:
67+ import mock
68+
69+
70+def callback_stub(*ignore):
71+ pass
72+
73+
74+class TestMenu(unittest.TestCase):
75+ """Test MenuManager class."""
76+
77+ def test_unread_count(self):
78+ menu = MenuManager(callback_stub, callback_stub)
79+ menu.launcher = mock.MagicMock()
80+
81+ expected = [mock.call('count', 42), mock.call('count_visible', True)]
82+ menu.update_unread_count(42)
83+ self.assertEqual(menu.launcher.set_property.call_args_list, expected)
84+
85+ menu.launcher.reset_mock()
86+ expected = [mock.call('count', 0), mock.call('count_visible', False)]
87+ menu.update_unread_count(0)
88+ self.assertEqual(menu.launcher.set_property.call_args_list, expected)
89+
90
91=== added file 'gwibber/gwibber/utils/menus.py'
92--- gwibber/gwibber/utils/menus.py 1970-01-01 00:00:00 +0000
93+++ gwibber/gwibber/utils/menus.py 2012-08-30 21:45:21 +0000
94@@ -0,0 +1,99 @@
95+# Copyright (C) 2012 Canonical Ltd
96+#
97+# This program is free software: you can redistribute it and/or modify
98+# it under the terms of the GNU General Public License version 2 as
99+# published by the Free Software Foundation.
100+#
101+# This program is distributed in the hope that it will be useful,
102+# but WITHOUT ANY WARRANTY; without even the implied warranty of
103+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
104+# GNU General Public License for more details.
105+#
106+# You should have received a copy of the GNU General Public License
107+# along with this program. If not, see <http://www.gnu.org/licenses/>.
108+
109+"""This file defines the Dbusmenu behavior used in Unity."""
110+
111+import subprocess
112+
113+from gettext import gettext as _
114+
115+try:
116+ from gi.repository import Unity, Dbusmenu
117+except:
118+ Unity = None
119+ Dbusmenu = None
120+
121+
122+try:
123+ from gi.repository import MessagingMenu
124+except:
125+ MessagingMenu = None
126+
127+
128+class MenuManager:
129+ messaging = None
130+ launcher = None
131+
132+ def __init__(self, refresh_callback, shutdown_callback):
133+ self._refresh = refresh_callback
134+ self._shutdown = shutdown_callback
135+
136+ if MessagingMenu:
137+ self.init_messaging_menu()
138+ if Unity and Dbusmenu:
139+ self.init_dbus_menu()
140+
141+ def init_messaging_menu(self):
142+ self.messaging = MessagingMenu.App(desktop_id='gwibber.desktop')
143+ self.messaging.register()
144+
145+ def init_dbus_menu(self):
146+ self.launcher = Unity.LauncherEntry.get_for_desktop_id('gwibber.desktop')
147+ ql = Dbusmenu.Menuitem.new()
148+
149+ post_menu = Dbusmenu.Menuitem.new()
150+ post_menu.property_set(Dbusmenu.MENUITEM_PROP_LABEL, _('Update Status'))
151+ post_menu.connect('item-activated', lambda *ignore:
152+ subprocess.Popen('gwibber-poster', shell=False))
153+
154+ refresh_menu = Dbusmenu.Menuitem.new()
155+ refresh_menu.property_set(Dbusmenu.MENUITEM_PROP_LABEL, _('Refresh'))
156+ refresh_menu.connect('item-activated', self._refresh)
157+
158+ preferences_menu = Dbusmenu.Menuitem.new()
159+ preferences_menu.property_set(Dbusmenu.MENUITEM_PROP_LABEL, _('Preferences'))
160+ preferences_menu.connect('item-activated', lambda *ignore:
161+ subprocess.Popen('gwibber-preferences', shell=False))
162+
163+ quit_menu = Dbusmenu.Menuitem.new()
164+ quit_menu.property_set(Dbusmenu.MENUITEM_PROP_LABEL, _('Quit'))
165+ quit_menu.connect('item-activated', self._shutdown)
166+
167+ for menu in (post_menu, refresh_menu, preferences_menu, quit_menu):
168+ menu.property_set_bool(Dbusmenu.MENUITEM_PROP_VISIBLE, True)
169+ ql.child_append(menu)
170+
171+ self.launcher.set_property('quicklist', ql)
172+ self.update_unread_count(0)
173+
174+ def update_unread_count(self, count):
175+ if self.launcher:
176+ self.launcher.set_property('count', count)
177+ self.launcher.set_property('count_visible', bool(count))
178+
179+# XXX This bit allows you to test this file by running it. This doesn't
180+# fit very well into the larger testsuite architecture so we could
181+# probably improve this somehow, but I'm not sure how.
182+if __name__ == '__main__':
183+ def stub(*ignore):
184+ pass
185+
186+ from gi.repository import GObject
187+ loop = GObject.MainLoop()
188+ menu = MenuManager(stub, stub)
189+
190+ # If you see the number 20 on your Gwibber icon,
191+ # then basically the test has succeeded.
192+ menu.update_unread_count(20)
193+ loop.run()
194
195=== modified file 'gwibber/microblog/dispatcher.py'
196--- gwibber/microblog/dispatcher.py 2012-08-29 20:01:19 +0000
197+++ gwibber/microblog/dispatcher.py 2012-08-30 21:45:21 +0000
198@@ -17,12 +17,6 @@
199 from .util.const import *
200 import subprocess
201
202-try:
203- from gi.repository import Unity, Dbusmenu
204-except:
205- Unity = None
206- Dbusmenu = None
207-
208 from gi.repository import Gio
209 from gi.repository import Accounts
210 import collections
211@@ -34,11 +28,6 @@
212 except:
213 pass
214
215-try:
216- from gi.repository import MessagingMenu
217-except:
218- MessagingMenu = None
219-
220 GLib.threads_init()
221
222 # Dynamically build a list of available service plugins
223@@ -269,45 +258,8 @@
224 for s in "messages", "replies", "private":
225 self.unseen_counts[s] = 0
226
227- self.mmapp = None
228- self.launcher = None
229-
230 self.job_list = []
231
232- if MessagingMenu:
233- self.mmapp = MessagingMenu.App(desktop_id='gwibber.desktop')
234- self.mmapp.register()
235- self.mmapp.connect('activate-source', self.on_messaging_menu_source_activated)
236- self.update_message_sources(self.unseen_counts)
237-
238- if Unity and Dbusmenu:
239- self.launcher = Unity.LauncherEntry.get_for_desktop_id ("gwibber.desktop")
240- ql = Dbusmenu.Menuitem.new ()
241- ql_post_menu = Dbusmenu.Menuitem.new ()
242- ql_post_menu.property_set (Dbusmenu.MENUITEM_PROP_LABEL, _("Update Status"))
243- ql_post_menu.property_set_bool (Dbusmenu.MENUITEM_PROP_VISIBLE, True)
244- ql_post_menu.connect("item-activated", self.show_poster)
245-
246- ql.child_append (ql_post_menu)
247- refresh_menu = Dbusmenu.Menuitem.new ()
248- refresh_menu.property_set (Dbusmenu.MENUITEM_PROP_LABEL, _("Refresh"))
249- refresh_menu.property_set_bool (Dbusmenu.MENUITEM_PROP_VISIBLE, True)
250- refresh_menu.connect("item-activated", self.refresh)
251- ql.child_append (refresh_menu)
252- preferences_menu = Dbusmenu.Menuitem.new ()
253- preferences_menu.property_set (Dbusmenu.MENUITEM_PROP_LABEL, _("Preferences"))
254- preferences_menu.property_set_bool (Dbusmenu.MENUITEM_PROP_VISIBLE, True)
255- preferences_menu.connect("item-activated", self.show_preferences)
256- ql.child_append (preferences_menu)
257- quit_menu = Dbusmenu.Menuitem.new ()
258- quit_menu.property_set (Dbusmenu.MENUITEM_PROP_LABEL, _("Quit"))
259- quit_menu.property_set_bool (Dbusmenu.MENUITEM_PROP_VISIBLE, True)
260- quit_menu.connect("item-activated", self.shutdown)
261- ql.child_append (quit_menu)
262- self.launcher.set_property("quicklist", ql)
263- self.launcher.set_property("count", 0)
264- self.launcher.set_property("count_visible", False)
265-
266 self.refresh_count = 0
267
268 self.refresh_timer_id = None
269@@ -345,27 +297,6 @@
270 return account
271 raise KeyError
272
273- def show_client(self, stream=None, *args):
274- cmd = []
275- cmd.append("gwibber")
276- if stream:
277- cmd.append("-s")
278- cmd.append(stream)
279- subprocess.Popen(cmd, shell=False)
280-
281- def show_preferences(self, *args):
282- subprocess.Popen("gwibber-preferences", shell=False)
283-
284- def show_accounts(self, *args):
285- subprocess.Popen("gwibber-accounts", shell=False)
286-
287- def show_poster(self, *args):
288- subprocess.Popen("gwibber-poster", shell=False)
289-
290- def shutdown(self, *args):
291- subprocess.Popen(["pkill", "gwibber"], shell=False)
292- self.Quit()
293-
294 def do_maintenance(self, *args):
295 # perform some needed MessageManager maintenance
296 if self.maint_timer_id:
297@@ -725,66 +656,12 @@
298 self.LoadingComplete()
299 logger.info("Loading complete: %s - %s", self.refresh_count, output.rowcount if output.rowcount > 0 else 0)
300
301- def update_message_sources(self, counts):
302- total_unseen = 0
303- pos = 0
304- if self.mmapp:
305- for source, name in self.stream_names.items():
306- if source in counts and counts[source] > 0:
307- if self.mmapp.has_source(source):
308- self.mmapp.set_source_count(source, counts[source])
309- else:
310- self.mmapp.insert_source_with_count(pos, source, None, name, counts[source])
311- total_unseen += counts[source]
312- pos += 1
313- if Unity and self.launcher:
314- self.launcher.set_property("count", total_unseen)
315- if total_unseen < 1:
316- self.launcher.set_property("count_visible", False)
317- else:
318- self.launcher.set_property("count_visible", True)
319- return False
320-
321- def on_messaging_menu_source_activated(self, mmapp, stream):
322- logger.debug("Raising gwibber client, focusing %s stream", stream)
323- try:
324- self.handle_message_source_counts(stream)
325- except:
326- pass
327- self.show_client(stream=stream)
328-
329 def handle_focus_reply(self, *args):
330 logger.debug("Gwibber Client raised")
331
332 def handle_focus_error(self, *args):
333 logger.error("Failed to raise client %s", args)
334
335- def handle_message_source_counts(self, stream=None):
336- if self.mmapp:
337- if not stream or stream == "home":
338- for s in self.stream_names.keys():
339- self.mmapp.remove_source(s)
340- if self.launcher:
341- self.launcher.set_property("count", 0)
342- self.launcher.set_property("count_visible", False)
343- return
344- else:
345- self.mmapp.remove_source(stream)
346-
347- self.unseen_counts[stream] = 0
348- if Unity:
349- total_unseen = 0
350- for s in self.unseen_counts.keys():
351- total_unseen += self.unseen_counts[s]
352- logger.debug ("handle_message_source_counts total_unseen is %d", total_unseen)
353- if self.launcher:
354- self.launcher.set_property("count", total_unseen)
355- if total_unseen < 1:
356- self.launcher.set_property("count_visible", False)
357- else:
358- self.launcher.set_property("count_visible", True)
359-
360-
361 def new_search_message(self, data):
362 message = json.loads(data[-1])
363 GLib.idle_add(self.cache_avatar, message)

Subscribers

People subscribed via source and target branches