Merge lp:~robru/gwibber/account-manager into lp:~barry/gwibber/py3

Proposed by Robert Bruce Park
Status: Merged
Merged at revision: 1422
Proposed branch: lp:~robru/gwibber/account-manager
Merge into: lp:~barry/gwibber/py3
Diff against target: 221 lines (+118/-22)
3 files modified
gwibber/Makefile (+10/-0)
gwibber/gwibber/tests/test_account.py (+53/-17)
gwibber/gwibber/utils/account.py (+55/-5)
To merge this branch: bzr merge lp:~robru/gwibber/account-manager
Reviewer Review Type Date Requested Status
Barry Warsaw Pending
Review via email: mp+123347@code.launchpad.net

Description of the change

This branch adds AccountManager class in account.py, with code largely stolen directly from the old dispatcher. Some basic tests are added that cover its interaction with libaccounts and also with our streams.

Note that the AccountManager is not actually used anywhere yet, the intention is that it will be instantiated by the Dispatcher once that's in place. The reason for this is that it needs a callback into the Dispatcher's refresh() method in order to add new streams when new accounts have been added by libaccounts.

And just for fun, I've introduced a Makefile that makes it easier to invoke the testsuite, with or without virtualenv.

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

On Sep 07, 2012, at 07:16 PM, Robert Bruce Park wrote:

>This branch adds AccountManager class in account.py, with code largely stolen
>directly from the old dispatcher. Some basic tests are added that cover its
>interaction with libaccounts and also with our streams.

Nice.

>Note that the AccountManager is not actually used anywhere yet, the intention
>is that it will be instantiated by the Dispatcher once that's in place. The
>reason for this is that it needs a callback into the Dispatcher's refresh()
>method in order to add new streams when new accounts have been added by
>libaccounts.

Sounds good.

>And just for fun, I've introduced a Makefile that makes it easier to invoke
>the testsuite, with or without virtualenv.

Very convenient!

Okay, code review coming up. Just in general, I added a bunch of comments
throughout based on my understanding of the changes. Once your branch is
merged please take a look to make sure I'm not mistaken.

=== added file 'gwibber/Makefile'
--- gwibber/Makefile 1970-01-01 00:00:00 +0000
+++ gwibber/Makefile 2012-09-07 19:15:22 +0000
> @@ -0,0 +1,10 @@
> +check:
> + python3 -m unittest discover -vv
> +
> +install:
> + python3 setup.py install
> +
> +check_all:
> + virtualenv --system-site-packages -p python3 /tmp/gwib
> + /tmp/gwib/bin/python3 setup.py install
> + /tmp/gwib/bin/python3 -m unittest discover -vv

This is great. I'm just going to add a standard copyright notice to the top
of the file.

=== modified file 'gwibber/gwibber/tests/test_account.py'
--- gwibber/gwibber/tests/test_account.py 2012-09-06 20:48:07 +0000
+++ gwibber/gwibber/tests/test_account.py 2012-09-07 19:15:22 +0000
> @@ -21,9 +21,12 @@
>
> import unittest
>
> +from gi.repository import Accounts
> +

Turns out this import isn't used, so I've removed it. Have you seen the
pyflakes tool? It's a nice little tool that finds some common mistakes in
Python code. It's not as thorough as others like pylint, but also doesn't
have the vast number of false positives that pylint can produce by default
(and which are a pita to suppress). Of course pylint integrates nicely with
Emacs so when it complains, it paints the line with a red background so it's
easy to see. It also displays the number of problems it finds in the
modeline.

For me, pyflakes strikes a nice balance for a static Python checker that gives
you useful warnings without bombarding you with false positives. A lot of
folks also like the pep8 tool, but since I co-authored that PEP, I tend to
have it's rules engraved in my brain, as you've no doubt unfortunately
observed in my curmudgeonly reviews. :)

> from gwibber.errors import UnsupportedProtocolError
> from gwibber.protocols.flickr import Flickr
> -from gwibber.utils.account import Account
> +from gwibber.utils.account import Account, AccountManager
> +
>
> try:
> # Python 3.3
> @@ -32,6 +35,30 @@
> import mock
>
>
> +accounts_manager = mock.Mock()
> +accounts_manager.new_for_service_type(
> + 'microblogging').get_enabled_account_services.return_value = []
> +
> +
> +class FakeAccount(dict):
> + global_id = 'some fake id'
> + def __init__(self, service):
> + self['id'] = 'faker/tha...

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

On 12-09-07 04:52 PM, Barry Warsaw wrote:
>> +check_all:
>> + virtualenv --system-site-packages -p python3 /tmp/gwib
>> + /tmp/gwib/bin/python3 setup.py install
>> + /tmp/gwib/bin/python3 -m unittest discover -vv
>
> This is great. I'm just going to add a standard copyright notice to the top
> of the file.

I'm glad you like it. I got sick of grepping through #gwibber logs to
try and figure out how to re-start the venv every day ;-)

>> +from gi.repository import Accounts
>> +
>
> Turns out this import isn't used, so I've removed it.

Oh, oops. I thought it was necessary in order for mock.patch to patch
over it.

> Have you seen the
> pyflakes tool? It's a nice little tool that finds some common mistakes in
> Python code. It's not as thorough as others like pylint, but also doesn't
> have the vast number of false positives that pylint can produce by default
> (and which are a pita to suppress). Of course pylint integrates nicely with
> Emacs so when it complains, it paints the line with a red background so it's
> easy to see. It also displays the number of problems it finds in the
> modeline.

Never heard of pyflakes, but I have used pylint on past projects. I know
all too well what you mean about suppressing pylint false positives:

https://github.com/robru/gottengeography/blob/gexiv2/delinter.sh

;-)

> For me, pyflakes strikes a nice balance for a static Python checker that gives
> you useful warnings without bombarding you with false positives. A lot of
> folks also like the pep8 tool, but since I co-authored that PEP, I tend to
> have it's rules engraved in my brain, as you've no doubt unfortunately
> observed in my curmudgeonly reviews. :)

Whoa, I actually was not aware that you co-authored PEP8. I had no idea
I was working with such a prominent figure in the Python community. I'm
honored! ;-)

>> +class FakeAccount(dict):
>> + global_id = 'some fake id'
>> + def __init__(self, service):
>> + self['id'] = 'faker/than fake'
>
> As it turns out, I just added a testing helper for fake accounts, so I rolled
> this change into there.

Fair. This one was necessary because, as it turns out, mock doesn't
support subscripting syntax at all. I was quite surprised, considering
how magical it tends to be with everything else. you'd think they could
have written a __setitem__ that was just as magical, but I guess not.
Who knows.

>> +class SettingsIterMock:
>> + """Mimick the weird libaccounts get_settings_iter."""
>> +
>> + def __init__(self):
>> + self.items = [(True, 'key', 'value')]
>> +
>> + def next(self):
>> + if self.items:
>> + return self.items.pop()
>> + else:
>> + return (False, 'dog', 'four')
>
> Yes, this looks like a much better model of AccountSettingsIter, except for
> that as I observe it, when the iterator is exhausted, (False, None, None) is
> actually returned. Not that it matters since the second and third items will
> be ignored, but see some commentary below.

Yes, it is (False, None, None), I just copied the 'dog', 'four' stuff in
keeping with the theme of the test that you'd written.

>> + @mock.patch('gi.repository.Accounts.Manager', acc...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'gwibber/Makefile'
2--- gwibber/Makefile 1970-01-01 00:00:00 +0000
3+++ gwibber/Makefile 2012-09-07 19:15:22 +0000
4@@ -0,0 +1,10 @@
5+check:
6+ python3 -m unittest discover -vv
7+
8+install:
9+ python3 setup.py install
10+
11+check_all:
12+ virtualenv --system-site-packages -p python3 /tmp/gwib
13+ /tmp/gwib/bin/python3 setup.py install
14+ /tmp/gwib/bin/python3 -m unittest discover -vv
15
16=== modified file 'gwibber/gwibber/tests/test_account.py'
17--- gwibber/gwibber/tests/test_account.py 2012-09-06 20:48:07 +0000
18+++ gwibber/gwibber/tests/test_account.py 2012-09-07 19:15:22 +0000
19@@ -21,9 +21,12 @@
20
21 import unittest
22
23+from gi.repository import Accounts
24+
25 from gwibber.errors import UnsupportedProtocolError
26 from gwibber.protocols.flickr import Flickr
27-from gwibber.utils.account import Account
28+from gwibber.utils.account import Account, AccountManager
29+
30
31 try:
32 # Python 3.3
33@@ -32,6 +35,30 @@
34 import mock
35
36
37+accounts_manager = mock.Mock()
38+accounts_manager.new_for_service_type(
39+ 'microblogging').get_enabled_account_services.return_value = []
40+
41+
42+class FakeAccount(dict):
43+ global_id = 'some fake id'
44+ def __init__(self, service):
45+ self['id'] = 'faker/than fake'
46+
47+
48+class SettingsIterMock:
49+ """Mimick the weird libaccounts get_settings_iter."""
50+
51+ def __init__(self):
52+ self.items = [(True, 'key', 'value')]
53+
54+ def next(self):
55+ if self.items:
56+ return self.items.pop()
57+ else:
58+ return (False, 'dog', 'four')
59+
60+
61 class TestAccount(unittest.TestCase):
62 """Test Account class."""
63
64@@ -54,9 +81,7 @@
65 'get_parameters.return_value': 'fake parameters',
66 }),
67 'get_account.return_value': mock.Mock(**{
68- 'get_settings_iter.return_value': [
69- (True, 'key', 'value'),
70- ],
71+ 'get_settings_iter.return_value': SettingsIterMock(),
72 'id': 'fake_id',
73 'get_provider_name.return_value': 'flickr',
74 }),
75@@ -111,20 +136,14 @@
76 # received, the callback is called and the account is updated.
77 #
78 # Start by simulating a change in the account service.
79- iter = self.account_service.get_account().get_settings_iter
80- iter.return_value = [
81+ other_iter = SettingsIterMock()
82+ other_iter.items = [
83 (True, 'ant', 'one'),
84 (True, 'bee', 'two'),
85 (True, 'cat', 'three'),
86- # The semantics of get_settings_iter() is that it will stop
87- # returning values on the first False in item[0] it encounters.
88- # Since the mock doesn't completely mimic this behavior, all we
89- # can do is test that False values don't make it into the
90- # account. IOW, it should be impossible for True values to follow
91- # False ones, but that's behavior in get_settings_iter() and we
92- # can't really (nor should we) test that.
93- (False, 'dog', 'four'),
94 ]
95+ iter = self.account_service.get_account().get_settings_iter
96+ iter.return_value = other_iter
97 # Check that the signal has been connected.
98 self.assertEqual(self._callback_signal, 'changed')
99 # Check that the account is the object we expect it to be.
100@@ -155,9 +174,7 @@
101 # other mock service has to at least support the basic required API.
102 other = Account(mock.Mock(**{
103 'get_account.return_value': mock.Mock(**{
104- 'get_settings_iter.return_value': [
105- (True, 'key', 'value'),
106- ],
107+ 'get_settings_iter.return_value': SettingsIterMock(),
108 # It's okay if the provider names are the same; the test
109 # is for whether the account services are the same or not,
110 # and in this test, they'll be different mock instances.
111@@ -165,3 +182,22 @@
112 }),
113 }))
114 self.assertNotEqual(self.account, other)
115+
116+ @mock.patch('gi.repository.Accounts.Manager', accounts_manager)
117+ @mock.patch('gwibber.utils.account.Account', FakeAccount)
118+ def test_account_manager(self):
119+ mock_stream = mock.Mock()
120+ mock_stream.List.return_value = \
121+ '[{"account":"faker/than fake","id":"foo"}]'
122+ account_service = mock.Mock()
123+
124+ manager = AccountManager(lambda:12345, mock_stream)
125+ self.assertEqual(manager.refresh(), 12345)
126+
127+ manager.add_new_account(account_service)
128+ self.assertIn('some fake id', manager.accounts)
129+
130+ manager.on_account_deleted(accounts_manager, 'some fake id')
131+ self.assertNotIn('some fake id', manager.accounts)
132+ mock_stream.List.assert_called_once_with()
133+ mock_stream.Delete.assert_called_once_with('foo')
134
135=== modified file 'gwibber/gwibber/utils/account.py'
136--- gwibber/gwibber/utils/account.py 2012-09-06 20:48:07 +0000
137+++ gwibber/gwibber/utils/account.py 2012-09-07 19:15:22 +0000
138@@ -16,18 +16,63 @@
139
140 __all__ = [
141 'Account',
142+ 'AccountManager',
143 ]
144
145
146+import json
147+import logging
148+log = logging.getLogger('gwibber.accounts')
149+
150 from threading import Lock
151
152+from gi.repository import Accounts
153+
154 from gwibber.errors import UnsupportedProtocolError
155-# TODO later on we'll get the global ProtocolManager instance from the
156-# Dispatcher when it exists, but for now we need to import it here.
157 from gwibber.utils.protocol import ProtocolManager
158+
159 _manager = ProtocolManager()
160
161
162+class AccountManager:
163+ """Manage the accounts that we know about."""
164+
165+ def __init__(self, refresh_callback, streams):
166+ self.refresh = refresh_callback
167+ self.streams = streams
168+ self.accounts = {}
169+
170+ manager = Accounts.Manager.new_for_service_type('microblogging')
171+ manager.connect('enabled-event', self.on_enabled_event)
172+ manager.connect('account-deleted', self.on_account_deleted)
173+ for account_service in manager.get_enabled_account_services():
174+ self.add_new_account(account_service)
175+ log.info('Found {} accounts'.format(len(self.accounts)))
176+
177+ def on_enabled_event(self, manager, account_id):
178+ account = manager.get_account(account_id)
179+ for service in account.list_services():
180+ account_service = Accounts.AccountService.new(account, service)
181+ if account_service.get_enabled():
182+ self.add_new_account(account_service)
183+ self.refresh()
184+
185+ def on_account_deleted(self, manager, account_id):
186+ log.debug('Deleting account ' + account_id)
187+ account = self.accounts.pop(account_id)
188+ try:
189+ for stream in json.loads(self.streams.List()):
190+ if stream['account'] == account['id']:
191+ self.streams.Delete(stream['id'])
192+ except:
193+ # TODO Identify what actually gets raised so we can catch
194+ # it properly.
195+ raise
196+
197+ def add_new_account(self, account_service):
198+ new_account = Account(account_service)
199+ self.accounts[new_account.global_id] = new_account
200+
201 class Account(dict):
202 """A thin wrapper around libaccounts API."""
203
204@@ -60,9 +105,14 @@
205
206 def on_account_changed(self, account_service, account):
207 settings = account.get_settings_iter('gwibber/')
208- self.update((key, value)
209- for (success, key, value) in settings
210- if success)
211+ # This is a little bit ugly, but necessary until we get around
212+ # to making the libaccounts API a little bit nicer.
213+ while True:
214+ success, key, value = settings.next()
215+ if success:
216+ self[key] = value
217+ else:
218+ break
219
220 def enabled(self):
221 return self.account_service.get_enabled()

Subscribers

People subscribed via source and target branches