Merge lp:~robru/gwibber/dbusmenu into lp:~barry/gwibber/py3
- dbusmenu
- Merge into py3
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Barry Warsaw | Pending | ||
Review via email: mp+122139@code.launchpad.net |
Commit message
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.
Barry Warsaw (barry) wrote : | # |
Robert Bruce Park (robru) wrote : | # |
On 12-08-30 04:05 PM, Barry Warsaw wrote:>> +def refresh_
>> + # TODO: Once the new Dispatcher is born, this method should trigger
>> + # it to do a refresh.
>> + pass
>> +
>> +def shutdown_
>> + # 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.
>> + post_menu.
_('Update Status'))
>> + post_menu.
>> + subprocess.
>
> 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.
> 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...
- 1406. By Robert Bruce Park
-
Add a basic unit test for the dbusmenu.
- 1407. By Robert Bruce Park
-
Private refresh and shutdown callbacks.
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
- 1408. By Robert Bruce Park
-
Reduce ugliness in main.py.
Preview Diff
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) |
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 unread_ count() is called. That
the right calls have been made when update_
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 testing/ service. py which would take an integer and turn around and unread_ count() with that value. The problem is, I can't think of
add a test for this is to add a dbus call to the TestService in
gwibber/
call update_
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/ main.py 2012-08-21 22:42:24 +0000 gwibber/ main.py 2012-08-30 20:10:23 +0000 xdg_config_ home, 'gwibber', 'gwibber.sqlite') callback( *ignore) : callback( *ignore) :
--- gwibber/
+++ gwibber/
> @@ -46,6 +47,16 @@
> DEFAULT_SQLITE_FILE = os.path.join(
> BaseDirectory.
>
> +def refresh_
> + # TODO: Once the new Dispatcher is born, this method should trigger
> + # it to do a refresh.
> + pass
> +
> +def shutdown_
> + # 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/ utils/menus. py 1970-01-01 00:00:00 +0000 gwibber/ utils/menus. py 2012-08-30 20:10:23 +0000 www.gnu. org/licenses/>.
--- gwibber/
+++ gwibber/
> @@ -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://
> +
> +"""This file defines the Dbusmenu behavior used in Unity."""
> +
> +import subprocess
> +
> +from gettext import gettext as _
> +
> +try:
> + from gi.repository import Unity, D...