Merge lp:~robru/gwibber/locked-login-refactor into lp:~barry/gwibber/py3

Proposed by Robert Bruce Park
Status: Merged
Merged at revision: 1449
Proposed branch: lp:~robru/gwibber/locked-login-refactor
Merge into: lp:~barry/gwibber/py3
Diff against target: 183 lines (+39/-59)
5 files modified
gwibber/gwibber/protocols/flickr.py (+5/-17)
gwibber/gwibber/protocols/foursquare.py (+8/-21)
gwibber/gwibber/protocols/twitter.py (+5/-18)
gwibber/gwibber/tests/test_flickr.py (+1/-1)
gwibber/gwibber/utils/base.py (+20/-2)
To merge this branch: bzr merge lp:~robru/gwibber/locked-login-refactor
Reviewer Review Type Date Requested Status
Barry Warsaw Pending
Review via email: mp+128805@code.launchpad.net

Description of the change

Barry, I split this into a separate mp just for you. No rush, Facebook won't be ready today anyway. ;-)

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

On Oct 09, 2012, at 08:03 PM, Robert Bruce Park wrote:

>Barry, I split this into a separate mp just for you. No rush, Facebook won't
>be ready today anyway. ;-)

This looks great, and I'm ready to merge it, but I have one question.

> - data = get_json(SELF_URL.format(access_token=token)) or {}

I see this pattern a lot, but I wonder why. Is it expected that get_json()
would return a false value that *wasn't* an empty dictionary?

I'm very tempted to take out the "or {}" unless there's a concrete reason
to add it back (read: failing test :).

Revision history for this message
Robert Bruce Park (robru) wrote :

On 12-10-10 09:38 AM, Barry Warsaw wrote:
> This looks great, and I'm ready to merge it, but I have one question.
>
>> - data = get_json(SELF_URL.format(access_token=token)) or {}
>
> I see this pattern a lot, but I wonder why. Is it expected that get_json()
> would return a false value that *wasn't* an empty dictionary?

Well, just like the millions of places where I write .get('foo') instead
of ['foo'], it's written that way to deliberately be fault-tolerant
against *unknown* error conditions.

> I'm very tempted to take out the "or {}" unless there's a concrete reason
> to add it back (read: failing test :).

Well, if you want to make the code *more* brittle, I think it's up to
you to point at some API reference documentation that indicates that the
return value will *always* be some JSON dict regardless of any error
condition that might arise. I think the onus is on you to prove that
your change is safe, not on me to prove that this clearly-harmless edge
case won't save our butts at some point.

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

On Oct 10, 2012, at 04:34 PM, Robert Bruce Park wrote:

>On 12-10-10 09:38 AM, Barry Warsaw wrote:
>> This looks great, and I'm ready to merge it, but I have one question.
>>
>>> - data = get_json(SELF_URL.format(access_token=token)) or {}
>>
>> I see this pattern a lot, but I wonder why. Is it expected that get_json()
>> would return a false value that *wasn't* an empty dictionary?
>
>Well, just like the millions of places where I write .get('foo') instead
>of ['foo'], it's written that way to deliberately be fault-tolerant
>against *unknown* error conditions.

That's different. In this latter case, you can be sure of the data type of
object you're accessing, but you can't be sure of the contents of the keys.
Of course, there are infinite number of unknown error conditions, so you can't
possibly defend against them all. Defending against missing keys is a
reasonable trade-off IMHO.

>> I'm very tempted to take out the "or {}" unless there's a concrete reason
>> to add it back (read: failing test :).
>
>Well, if you want to make the code *more* brittle, I think it's up to
>you to point at some API reference documentation that indicates that the
>return value will *always* be some JSON dict regardless of any error
>condition that might arise. I think the onus is on you to prove that
>your change is safe, not on me to prove that this clearly-harmless edge
>case won't save our butts at some point.

I think that's akin to trying to prove a negative. Taken to the extreme,
potentially any data type or value could be returned from get_json(), but it's
impossible to defend against them. As an example, it's probably entirely
possible that a non-empty list, or the integer 7 is returned from get_json(),
and in either of those cases, the original code would *still* fail because the
'or' clause wouldn't get triggered. It's only in the narrow case of a
non-true value (e.g. 0 or None, or the empty string) that the 'or' case would
get triggered.

In situations like this, I prefer to react to bugs so that we know exactly
what situation can cause the non-true return value to occur. In any event, a
test case would be useful.

You could argue that maybe in the .get() vs [] situation above we should do
the same - wait until a real-world bug arrives before we write defensive
code. That could be convincing, but the cost of adding defensive code in
those cases is so much higher because of the number of call sites that would
have to be fixed, so it seems like a better trade-off.
y

Revision history for this message
Robert Bruce Park (robru) wrote :

On 12-10-10 12:40 PM, Barry Warsaw wrote:
> On Oct 10, 2012, at 04:34 PM, Robert Bruce Park wrote:
>> Well, if you want to make the code *more* brittle, I think it's up to
>> you to point at some API reference documentation that indicates that the
>> return value will *always* be some JSON dict regardless of any error
>> condition that might arise. I think the onus is on you to prove that
>> your change is safe, not on me to prove that this clearly-harmless edge
>> case won't save our butts at some point.
>
> I think that's akin to trying to prove a negative. Taken to the extreme,
> potentially any data type or value could be returned from get_json(), but it's
> impossible to defend against them. As an example, it's probably entirely
> possible that a non-empty list, or the integer 7 is returned from get_json(),
> and in either of those cases, the original code would *still* fail because the
> 'or' clause wouldn't get triggered. It's only in the narrow case of a
> non-true value (e.g. 0 or None, or the empty string) that the 'or' case would
> get triggered.

Except that some unforeseen error condition that prevents the dict from
being returned is *much* more likely to return None instead of the
integer 7 or a non-empty list, because None is what you get when you
call return with no values. So I don't think a simple test against the
false-ness of the return value is particularly unreasonable, especially
when it is only 5 bytes of source code.

> In situations like this, I prefer to react to bugs so that we know exactly
> what situation can cause the non-true return value to occur. In any event, a
> test case would be useful.

Ok, but you made the change, so you write the test :-P

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

On Oct 10, 2012, at 06:40 PM, Robert Bruce Park wrote:

>Except that some unforeseen error condition that prevents the dict from
>being returned is *much* more likely to return None instead of the
>integer 7 or a non-empty list, because None is what you get when you
>call return with no values.

Except there's only one way out of get_json() and that's through json.loads().
So you *could* get None back, but you'd have to have gotten a response of just
the string 'null', since that's the only thing that results in None by
json.loads().

Yes, that's an error condition, but again, it's unlikely and you can't defend
against every possible response from a service.

>So I don't think a simple test against the false-ness of the return value is
>particularly unreasonable, especially when it is only 5 bytes of source code.
>
>> In situations like this, I prefer to react to bugs so that we know exactly
>> what situation can cause the non-true return value to occur. In any event,
>> a test case would be useful.
>
>Ok, but you made the change, so you write the test :-P

But in this case, there's nothing more to test. :)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'gwibber/gwibber/protocols/flickr.py'
2--- gwibber/gwibber/protocols/flickr.py 2012-09-25 20:25:42 +0000
3+++ gwibber/gwibber/protocols/flickr.py 2012-10-09 20:02:23 +0000
4@@ -22,7 +22,6 @@
5 import logging
6
7 from gwibber.errors import AuthorizationError
8-from gwibber.utils.authentication import Authentication
9 from gwibber.utils.base import Base, feature
10 from gwibber.utils.download import get_json
11 from gwibber.utils.time import iso8601utc, parsetime
12@@ -59,22 +58,11 @@
13 return None
14 return self._account.user_id
15
16- def _locked_login(self, old_token):
17- if old_token is None:
18- log.debug('Logging into Flickr')
19- else:
20- log.debug('Re-authenticating to Flickr')
21- result = Authentication(self._account).login()
22- if result is None:
23- log.error('No Flickr authentication results received')
24- elif 'AccessToken' in result:
25- self._account.user_name = result.get('username')
26- self._account.user_id = result.get('user_nsid')
27- self._account.access_token = result.get('AccessToken')
28- self._account.secret_token = result.get('TokenSecret')
29- log.debug('Flickr AccessToken received')
30- else:
31- log.error('No AccessToken in Flickr session: {!r}'.format(result))
32+ def _whoami(self, authdata):
33+ """Identify the authenticating user."""
34+ self._account.secret_token = authdata.get('TokenSecret')
35+ self._account.user_id = authdata.get('user_nsid')
36+ self._account.user_name = authdata.get('username')
37
38 # http://www.flickr.com/services/api/flickr.photos.getContactsPublicPhotos.html
39 @feature
40
41=== modified file 'gwibber/gwibber/protocols/foursquare.py'
42--- gwibber/gwibber/protocols/foursquare.py 2012-09-25 20:25:42 +0000
43+++ gwibber/gwibber/protocols/foursquare.py 2012-10-09 20:02:23 +0000
44@@ -22,7 +22,6 @@
45 import logging
46
47 from gwibber.errors import AuthorizationError
48-from gwibber.utils.authentication import Authentication
49 from gwibber.utils.base import Base, feature
50 from gwibber.utils.download import get_json
51 from gwibber.utils.time import iso8601utc
52@@ -55,26 +54,14 @@
53
54
55 class FourSquare(Base):
56- def _locked_login(self, old_token):
57- log.debug('{} to FourSquare'.format(
58- 'Re-authenticating' if old_token else 'Logging in'))
59-
60- result = Authentication(self._account, log).login()
61- if result is None:
62- log.error('No FourSquare authentication results received.')
63- return
64-
65- token = result.get('AccessToken')
66- if token is None:
67- log.error('No AccessToken in FourSquare session: {!r}', result)
68- else:
69- data = get_json(SELF_URL.format(access_token=token)) or {}
70- user = data.get('response', {}).get('user', {})
71- uid = user['id']
72- self._account.access_token = token
73- self._account.user_name = _full_name(user)
74- self._account.user_id = uid
75- log.debug('FourSquare UID is: {}', uid)
76+ def _whoami(self, authdata):
77+ """Identify the authenticating user."""
78+ data = get_json(SELF_URL.format(
79+ access_token=self._account.access_token)) or {}
80+ user = data.get('response', {}).get('user', {})
81+ self._account.secret_token = authdata.get('TokenSecret')
82+ self._account.user_name = _full_name(user)
83+ self._account.user_id = user.get('id')
84
85 @feature
86 def receive(self):
87
88=== modified file 'gwibber/gwibber/protocols/twitter.py'
89--- gwibber/gwibber/protocols/twitter.py 2012-10-05 17:58:23 +0000
90+++ gwibber/gwibber/protocols/twitter.py 2012-10-09 20:02:23 +0000
91@@ -28,7 +28,6 @@
92 from urllib.error import HTTPError
93 from urllib.parse import quote, urlsplit, urlunsplit
94
95-from gwibber.utils.authentication import Authentication
96 from gwibber.utils.base import Base, feature
97 from gwibber.utils.download import RateLimiter as BaseRateLimiter, get_json
98 from gwibber.utils.time import parsetime, iso8601utc
99@@ -65,23 +64,11 @@
100 super().__init__(account)
101 self._rate_limiter = RateLimiter()
102
103- def _locked_login(self, old_token):
104- """Sign in without worrying about concurrent login attempts."""
105- result = Authentication(self._account, log).login()
106- if result is None:
107- log.error('No Twitter authentication results received.')
108- return
109-
110- token = result.get('AccessToken')
111- if token is None:
112- log.error('No AccessToken in Twitter session: {!r}', result)
113- else:
114- self._account.access_token = token
115- self._account.secret_token = result.get('TokenSecret')
116- self._account.user_id = result.get('UserId')
117- self._account.user_name = result.get('ScreenName')
118- log.debug('{} UID: {}'.format(self.__class__.__name__,
119- self._account.user_id))
120+ def _whoami(self, authdata):
121+ """Identify the authenticating user."""
122+ self._account.secret_token = authdata.get('TokenSecret')
123+ self._account.user_id = authdata.get('UserId')
124+ self._account.user_name = authdata.get('ScreenName')
125
126 def _get_url(self, url, data=None):
127 """Access the Twitter API with correct OAuth signed headers."""
128
129=== modified file 'gwibber/gwibber/tests/test_flickr.py'
130--- gwibber/gwibber/tests/test_flickr.py 2012-09-25 20:25:42 +0000
131+++ gwibber/gwibber/tests/test_flickr.py 2012-10-09 20:02:23 +0000
132@@ -163,7 +163,7 @@
133 # AccessToken, but this fails.
134 self.protocol('receive')
135 self.assertEqual(self.log_mock.empty(), """\
136-No Flickr authentication results received
137+No Flickr authentication results received.
138 Flickr: No NSID available
139 Gwibber operation exception:
140 Traceback (most recent call last):
141
142=== modified file 'gwibber/gwibber/utils/base.py'
143--- gwibber/gwibber/utils/base.py 2012-10-04 19:29:35 +0000
144+++ gwibber/gwibber/utils/base.py 2012-10-09 20:02:23 +0000
145@@ -26,6 +26,7 @@
146 import logging
147 import threading
148
149+from gwibber.utils.authentication import Authentication
150 from gwibber.utils.model import COLUMN_INDICES, SCHEMA, DEFAULTS, Model
151
152
153@@ -289,11 +290,28 @@
154 return self._account.access_token != old_token
155
156 def _locked_login(self, old_token):
157- """Login operation stub. This must be implemented by subclasses."""
158- raise NotImplementedError
159+ """Sign in without worrying about concurrent login attempts."""
160+ cls = self.__class__.__name__
161+
162+ log.debug('{} to {}'.format(
163+ 'Re-authenticating' if old_token else 'Logging in', cls))
164+
165+ result = Authentication(self._account, log).login()
166+ if result is None:
167+ log.error('No {} authentication results received.'.format(cls))
168+ return
169+
170+ token = result.get('AccessToken')
171+ if token is None:
172+ log.error('No AccessToken in {} session: {!r}'.format(cls, result))
173+ else:
174+ self._account.access_token = token
175+ self._whoami(result)
176+ log.debug('{} UID: {}'.format(cls, self._account.user_id))
177
178 @classmethod
179 def get_features(cls):
180+ """Report what public operations we expose over DBus."""
181 features = []
182 for name in dir(cls):
183 if getattr(getattr(cls, name), 'is_feature', False):

Subscribers

People subscribed via source and target branches