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
=== modified file 'gwibber/gwibber/protocols/flickr.py'
--- gwibber/gwibber/protocols/flickr.py 2012-09-25 20:25:42 +0000
+++ gwibber/gwibber/protocols/flickr.py 2012-10-09 20:02:23 +0000
@@ -22,7 +22,6 @@
22import logging22import logging
2323
24from gwibber.errors import AuthorizationError24from gwibber.errors import AuthorizationError
25from gwibber.utils.authentication import Authentication
26from gwibber.utils.base import Base, feature25from gwibber.utils.base import Base, feature
27from gwibber.utils.download import get_json26from gwibber.utils.download import get_json
28from gwibber.utils.time import iso8601utc, parsetime27from gwibber.utils.time import iso8601utc, parsetime
@@ -59,22 +58,11 @@
59 return None58 return None
60 return self._account.user_id59 return self._account.user_id
6160
62 def _locked_login(self, old_token):61 def _whoami(self, authdata):
63 if old_token is None:62 """Identify the authenticating user."""
64 log.debug('Logging into Flickr')63 self._account.secret_token = authdata.get('TokenSecret')
65 else:64 self._account.user_id = authdata.get('user_nsid')
66 log.debug('Re-authenticating to Flickr')65 self._account.user_name = authdata.get('username')
67 result = Authentication(self._account).login()
68 if result is None:
69 log.error('No Flickr authentication results received')
70 elif 'AccessToken' in result:
71 self._account.user_name = result.get('username')
72 self._account.user_id = result.get('user_nsid')
73 self._account.access_token = result.get('AccessToken')
74 self._account.secret_token = result.get('TokenSecret')
75 log.debug('Flickr AccessToken received')
76 else:
77 log.error('No AccessToken in Flickr session: {!r}'.format(result))
7866
79# http://www.flickr.com/services/api/flickr.photos.getContactsPublicPhotos.html67# http://www.flickr.com/services/api/flickr.photos.getContactsPublicPhotos.html
80 @feature68 @feature
8169
=== modified file 'gwibber/gwibber/protocols/foursquare.py'
--- gwibber/gwibber/protocols/foursquare.py 2012-09-25 20:25:42 +0000
+++ gwibber/gwibber/protocols/foursquare.py 2012-10-09 20:02:23 +0000
@@ -22,7 +22,6 @@
22import logging22import logging
2323
24from gwibber.errors import AuthorizationError24from gwibber.errors import AuthorizationError
25from gwibber.utils.authentication import Authentication
26from gwibber.utils.base import Base, feature25from gwibber.utils.base import Base, feature
27from gwibber.utils.download import get_json26from gwibber.utils.download import get_json
28from gwibber.utils.time import iso8601utc27from gwibber.utils.time import iso8601utc
@@ -55,26 +54,14 @@
5554
5655
57class FourSquare(Base):56class FourSquare(Base):
58 def _locked_login(self, old_token):57 def _whoami(self, authdata):
59 log.debug('{} to FourSquare'.format(58 """Identify the authenticating user."""
60 'Re-authenticating' if old_token else 'Logging in'))59 data = get_json(SELF_URL.format(
6160 access_token=self._account.access_token)) or {}
62 result = Authentication(self._account, log).login()61 user = data.get('response', {}).get('user', {})
63 if result is None:62 self._account.secret_token = authdata.get('TokenSecret')
64 log.error('No FourSquare authentication results received.')63 self._account.user_name = _full_name(user)
65 return64 self._account.user_id = user.get('id')
66
67 token = result.get('AccessToken')
68 if token is None:
69 log.error('No AccessToken in FourSquare session: {!r}', result)
70 else:
71 data = get_json(SELF_URL.format(access_token=token)) or {}
72 user = data.get('response', {}).get('user', {})
73 uid = user['id']
74 self._account.access_token = token
75 self._account.user_name = _full_name(user)
76 self._account.user_id = uid
77 log.debug('FourSquare UID is: {}', uid)
7865
79 @feature66 @feature
80 def receive(self):67 def receive(self):
8168
=== modified file 'gwibber/gwibber/protocols/twitter.py'
--- gwibber/gwibber/protocols/twitter.py 2012-10-05 17:58:23 +0000
+++ gwibber/gwibber/protocols/twitter.py 2012-10-09 20:02:23 +0000
@@ -28,7 +28,6 @@
28from urllib.error import HTTPError28from urllib.error import HTTPError
29from urllib.parse import quote, urlsplit, urlunsplit29from urllib.parse import quote, urlsplit, urlunsplit
3030
31from gwibber.utils.authentication import Authentication
32from gwibber.utils.base import Base, feature31from gwibber.utils.base import Base, feature
33from gwibber.utils.download import RateLimiter as BaseRateLimiter, get_json32from gwibber.utils.download import RateLimiter as BaseRateLimiter, get_json
34from gwibber.utils.time import parsetime, iso8601utc33from gwibber.utils.time import parsetime, iso8601utc
@@ -65,23 +64,11 @@
65 super().__init__(account)64 super().__init__(account)
66 self._rate_limiter = RateLimiter()65 self._rate_limiter = RateLimiter()
6766
68 def _locked_login(self, old_token):67 def _whoami(self, authdata):
69 """Sign in without worrying about concurrent login attempts."""68 """Identify the authenticating user."""
70 result = Authentication(self._account, log).login()69 self._account.secret_token = authdata.get('TokenSecret')
71 if result is None:70 self._account.user_id = authdata.get('UserId')
72 log.error('No Twitter authentication results received.')71 self._account.user_name = authdata.get('ScreenName')
73 return
74
75 token = result.get('AccessToken')
76 if token is None:
77 log.error('No AccessToken in Twitter session: {!r}', result)
78 else:
79 self._account.access_token = token
80 self._account.secret_token = result.get('TokenSecret')
81 self._account.user_id = result.get('UserId')
82 self._account.user_name = result.get('ScreenName')
83 log.debug('{} UID: {}'.format(self.__class__.__name__,
84 self._account.user_id))
8572
86 def _get_url(self, url, data=None):73 def _get_url(self, url, data=None):
87 """Access the Twitter API with correct OAuth signed headers."""74 """Access the Twitter API with correct OAuth signed headers."""
8875
=== modified file 'gwibber/gwibber/tests/test_flickr.py'
--- gwibber/gwibber/tests/test_flickr.py 2012-09-25 20:25:42 +0000
+++ gwibber/gwibber/tests/test_flickr.py 2012-10-09 20:02:23 +0000
@@ -163,7 +163,7 @@
163 # AccessToken, but this fails.163 # AccessToken, but this fails.
164 self.protocol('receive')164 self.protocol('receive')
165 self.assertEqual(self.log_mock.empty(), """\165 self.assertEqual(self.log_mock.empty(), """\
166No Flickr authentication results received166No Flickr authentication results received.
167Flickr: No NSID available167Flickr: No NSID available
168Gwibber operation exception:168Gwibber operation exception:
169 Traceback (most recent call last):169 Traceback (most recent call last):
170170
=== modified file 'gwibber/gwibber/utils/base.py'
--- gwibber/gwibber/utils/base.py 2012-10-04 19:29:35 +0000
+++ gwibber/gwibber/utils/base.py 2012-10-09 20:02:23 +0000
@@ -26,6 +26,7 @@
26import logging26import logging
27import threading27import threading
2828
29from gwibber.utils.authentication import Authentication
29from gwibber.utils.model import COLUMN_INDICES, SCHEMA, DEFAULTS, Model30from gwibber.utils.model import COLUMN_INDICES, SCHEMA, DEFAULTS, Model
3031
3132
@@ -289,11 +290,28 @@
289 return self._account.access_token != old_token290 return self._account.access_token != old_token
290291
291 def _locked_login(self, old_token):292 def _locked_login(self, old_token):
292 """Login operation stub. This must be implemented by subclasses."""293 """Sign in without worrying about concurrent login attempts."""
293 raise NotImplementedError294 cls = self.__class__.__name__
295
296 log.debug('{} to {}'.format(
297 'Re-authenticating' if old_token else 'Logging in', cls))
298
299 result = Authentication(self._account, log).login()
300 if result is None:
301 log.error('No {} authentication results received.'.format(cls))
302 return
303
304 token = result.get('AccessToken')
305 if token is None:
306 log.error('No AccessToken in {} session: {!r}'.format(cls, result))
307 else:
308 self._account.access_token = token
309 self._whoami(result)
310 log.debug('{} UID: {}'.format(cls, self._account.user_id))
294311
295 @classmethod312 @classmethod
296 def get_features(cls):313 def get_features(cls):
314 """Report what public operations we expose over DBus."""
297 features = []315 features = []
298 for name in dir(cls):316 for name in dir(cls):
299 if getattr(getattr(cls, name), 'is_feature', False):317 if getattr(getattr(cls, name), 'is_feature', False):

Subscribers

People subscribed via source and target branches