Merge lp:~robru/gwibber/locked-login-refactor into lp:~barry/gwibber/py3
- locked-login-refactor
- Merge into py3
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Barry Warsaw | Pending | ||
Review via email: mp+128805@code.launchpad.net |
Commit message
Description of the change
Barry, I split this into a separate mp just for you. No rush, Facebook won't be ready today anyway. ;-)
Barry Warsaw (barry) wrote : | # |
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(
>
> 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.
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(
>>
>> 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
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
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
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): |
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 :).