Merge lp:~benji/launchpadlib/delayed-keyring-import into lp:launchpadlib

Proposed by Benji York
Status: Merged
Merged at revision: 107
Proposed branch: lp:~benji/launchpadlib/delayed-keyring-import
Merge into: lp:launchpadlib
Diff against target: 185 lines (+46/-14)
4 files modified
src/launchpadlib/credentials.py (+15/-1)
src/launchpadlib/testing/helpers.py (+12/-2)
src/launchpadlib/tests/test_http.py (+12/-5)
src/launchpadlib/tests/test_launchpad.py (+7/-6)
To merge this branch: bzr merge lp:~benji/launchpadlib/delayed-keyring-import
Reviewer Review Type Date Requested Status
Henning Eggers (community) code Approve
Review via email: mp+45711@code.launchpad.net

Description of the change

The keyring package initializes the back end storage at import time. If
the back end chosen is KDE, a couple of bad things happen: a dialog box
pops up asking for access to KWallet and a SIGCHLD handler is registered
which interferes with subprocess completion notification.

This branch delays the keyring import until the keyring is needed,
mitigating these problems. This approach has been approved by Leonard
and Gary.

This branch also eliminates the launchpadlib's test's usage of the real
keyring package because it introduces fragility and environment
dependencies into the test. The test suite also has enforcement that
ensures that non-testing keyring fakes are never used.

One class of test (SimulatedResponsesTestCase) had an off-by-one error
that these fixes exposed, those have been fixed as well.

To post a comment you must log in.
Revision history for this message
Henning Eggers (henninge) wrote :

The changes look good, thank you. I just wonder how the definition of "credential_store_factory" relates to this. Is that supposed to be in here? Please have a look before you land this.

review: Approve (code)
Revision history for this message
Benji York (benji) wrote :

On Mon, Jan 10, 2011 at 12:51 PM, Henning Eggers
<email address hidden> wrote:
> Review: Approve code
> The changes look good, thank you. I just wonder how the definition of
> "credential_store_factory" relates to this. Is that supposed to be in here?
> Please have a look before you land this.

The credential_store_factory definition in question is for a class
that's used in tests. The credential_store_factory returns the place
where credentials should be stored, which for the tests in question is
on disk (as opposed to in the keyring).

Thanks for the review.
--
Benji York

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/launchpadlib/credentials.py'
--- src/launchpadlib/credentials.py 2011-01-05 13:31:26 +0000
+++ src/launchpadlib/credentials.py 2011-01-10 16:23:59 +0000
@@ -37,7 +37,6 @@
37from urlparse import urljoin37from urlparse import urljoin
38import webbrowser38import webbrowser
3939
40import keyring
41import simplejson40import simplejson
4241
43from lazr.restfulclient.errors import HTTPError42from lazr.restfulclient.errors import HTTPError
@@ -295,13 +294,28 @@
295 integrating third-party websites into Launchpad.294 integrating third-party websites into Launchpad.
296 """295 """
297296
297 @staticmethod
298 def _ensure_keyring_imported():
299 """Ensure the keyring module is imported (postponing side effects).
300
301 The keyring module initializes the environment-dependent backend at
302 import time (nasty). We want to avoid that initialization because it
303 may do things like prompt the user to unlock their password store
304 (e.g., KWallet).
305 """
306 if 'keyring' not in globals():
307 global keyring
308 import keyring
309
298 def do_save(self, credentials, unique_key):310 def do_save(self, credentials, unique_key):
299 """Store newly-authorized credentials in the keyring."""311 """Store newly-authorized credentials in the keyring."""
312 self._ensure_keyring_imported()
300 keyring.set_password(313 keyring.set_password(
301 'launchpadlib', unique_key, credentials.serialize())314 'launchpadlib', unique_key, credentials.serialize())
302315
303 def do_load(self, unique_key):316 def do_load(self, unique_key):
304 """Retrieve credentials from the keyring."""317 """Retrieve credentials from the keyring."""
318 self._ensure_keyring_imported()
305 credential_string = keyring.get_password(319 credential_string = keyring.get_password(
306 'launchpadlib', unique_key)320 'launchpadlib', unique_key)
307 if credential_string is not None:321 if credential_string is not None:
308322
=== modified file 'src/launchpadlib/testing/helpers.py'
--- src/launchpadlib/testing/helpers.py 2011-01-04 20:00:37 +0000
+++ src/launchpadlib/testing/helpers.py 2011-01-10 16:23:59 +0000
@@ -44,6 +44,14 @@
44 )44 )
4545
4646
47missing = object()
48
49
50def assert_keyring_not_imported():
51 assert getattr(launchpadlib.credentials, 'keyring', missing) is missing, (
52 'During tests the real keyring module should never be imported.')
53
54
47class NoNetworkAuthorizationEngine(RequestTokenAuthorizationEngine):55class NoNetworkAuthorizationEngine(RequestTokenAuthorizationEngine):
48 """An authorization engine that doesn't open a web browser.56 """An authorization engine that doesn't open a web browser.
4957
@@ -121,12 +129,14 @@
121129
122@contextmanager130@contextmanager
123def fake_keyring(fake):131def fake_keyring(fake):
124 original_keyring = launchpadlib.credentials.keyring132 """A context manager which injects a testing keyring implementation."""
133 # The real keyring package should never be imported during tests.
134 assert_keyring_not_imported()
125 launchpadlib.credentials.keyring = fake135 launchpadlib.credentials.keyring = fake
126 try:136 try:
127 yield137 yield
128 finally:138 finally:
129 launchpadlib.credentials.keyring = original_keyring139 del launchpadlib.credentials.keyring
130140
131141
132class FauxSocketModule:142class FauxSocketModule:
133143
=== modified file 'src/launchpadlib/tests/test_http.py'
--- src/launchpadlib/tests/test_http.py 2011-01-04 18:46:27 +0000
+++ src/launchpadlib/tests/test_http.py 2011-01-10 16:23:59 +0000
@@ -17,11 +17,13 @@
17"""Tests for the LaunchpadOAuthAwareHTTP class."""17"""Tests for the LaunchpadOAuthAwareHTTP class."""
1818
19from collections import deque19from collections import deque
20import tempfile
20import unittest21import unittest
2122
22from simplejson import dumps, JSONDecodeError23from simplejson import dumps, JSONDecodeError
2324
24from launchpadlib.errors import Unauthorized25from launchpadlib.errors import Unauthorized
26from launchpadlib.credentials import UnencryptedFileCredentialStore
25from launchpadlib.launchpad import (27from launchpadlib.launchpad import (
26 Launchpad,28 Launchpad,
27 LaunchpadOAuthAwareHttp,29 LaunchpadOAuthAwareHttp,
@@ -92,6 +94,11 @@
92 return SimulatedResponsesHttp(94 return SimulatedResponsesHttp(
93 deque(self.responses), self, self.authorization_engine, *args)95 deque(self.responses), self, self.authorization_engine, *args)
9496
97 @classmethod
98 def credential_store_factory(cls, credential_save_failed):
99 return UnencryptedFileCredentialStore(
100 tempfile.mkstemp()[1], credential_save_failed)
101
95102
96class SimulatedResponsesTestCase(unittest.TestCase):103class SimulatedResponsesTestCase(unittest.TestCase):
97 """Test cases that give fake responses to launchpad's HTTP requests."""104 """Test cases that give fake responses to launchpad's HTTP requests."""
@@ -166,7 +173,7 @@
166 self.assertEquals(self.engine.access_tokens_obtained, 0)173 self.assertEquals(self.engine.access_tokens_obtained, 0)
167 launchpad = SimulatedResponsesLaunchpad.login_with(174 launchpad = SimulatedResponsesLaunchpad.login_with(
168 'application name', authorization_engine=self.engine)175 'application name', authorization_engine=self.engine)
169 self.assertEquals(self.engine.access_tokens_obtained, 0)176 self.assertEquals(self.engine.access_tokens_obtained, 1)
170177
171 def test_bad_token(self):178 def test_bad_token(self):
172 """If our token is bad, we get another one."""179 """If our token is bad, we get another one."""
@@ -178,7 +185,7 @@
178 self.assertEquals(self.engine.access_tokens_obtained, 0)185 self.assertEquals(self.engine.access_tokens_obtained, 0)
179 launchpad = SimulatedResponsesLaunchpad.login_with(186 launchpad = SimulatedResponsesLaunchpad.login_with(
180 'application name', authorization_engine=self.engine)187 'application name', authorization_engine=self.engine)
181 self.assertEquals(self.engine.access_tokens_obtained, 1)188 self.assertEquals(self.engine.access_tokens_obtained, 2)
182189
183 def test_expired_token(self):190 def test_expired_token(self):
184 """If our token is expired, we get another one."""191 """If our token is expired, we get another one."""
@@ -191,7 +198,7 @@
191 self.assertEquals(self.engine.access_tokens_obtained, 0)198 self.assertEquals(self.engine.access_tokens_obtained, 0)
192 launchpad = SimulatedResponsesLaunchpad.login_with(199 launchpad = SimulatedResponsesLaunchpad.login_with(
193 'application name', authorization_engine=self.engine)200 'application name', authorization_engine=self.engine)
194 self.assertEquals(self.engine.access_tokens_obtained, 1)201 self.assertEquals(self.engine.access_tokens_obtained, 2)
195202
196 def test_delayed_error(self):203 def test_delayed_error(self):
197 """We get another token no matter when the error happens."""204 """We get another token no matter when the error happens."""
@@ -203,7 +210,7 @@
203 self.assertEquals(self.engine.access_tokens_obtained, 0)210 self.assertEquals(self.engine.access_tokens_obtained, 0)
204 launchpad = SimulatedResponsesLaunchpad.login_with(211 launchpad = SimulatedResponsesLaunchpad.login_with(
205 'application name', authorization_engine=self.engine)212 'application name', authorization_engine=self.engine)
206 self.assertEquals(self.engine.access_tokens_obtained, 1)213 self.assertEquals(self.engine.access_tokens_obtained, 2)
207214
208 def test_many_errors(self):215 def test_many_errors(self):
209 """We'll keep getting new tokens as long as tokens are the problem."""216 """We'll keep getting new tokens as long as tokens are the problem."""
@@ -216,7 +223,7 @@
216 self.assertEquals(self.engine.access_tokens_obtained, 0)223 self.assertEquals(self.engine.access_tokens_obtained, 0)
217 launchpad = SimulatedResponsesLaunchpad.login_with(224 launchpad = SimulatedResponsesLaunchpad.login_with(
218 'application name', authorization_engine=self.engine)225 'application name', authorization_engine=self.engine)
219 self.assertEquals(self.engine.access_tokens_obtained, 3)226 self.assertEquals(self.engine.access_tokens_obtained, 4)
220227
221 def test_other_unauthorized(self):228 def test_other_unauthorized(self):
222 """If the token is not at fault, a 401 error raises an exception."""229 """If the token is not at fault, a 401 error raises an exception."""
223230
=== modified file 'src/launchpadlib/tests/test_launchpad.py'
--- src/launchpadlib/tests/test_launchpad.py 2011-01-05 13:31:26 +0000
+++ src/launchpadlib/tests/test_launchpad.py 2011-01-10 16:23:59 +0000
@@ -37,6 +37,7 @@
37import launchpadlib.launchpad37import launchpadlib.launchpad
38from launchpadlib.launchpad import Launchpad38from launchpadlib.launchpad import Launchpad
39from launchpadlib.testing.helpers import (39from launchpadlib.testing.helpers import (
40 assert_keyring_not_imported,
40 BadSaveKeyring,41 BadSaveKeyring,
41 fake_keyring,42 fake_keyring,
42 FauxSocketModule,43 FauxSocketModule,
@@ -197,15 +198,15 @@
197 """Base class for tests that use the keyring."""198 """Base class for tests that use the keyring."""
198199
199 def setUp(self):200 def setUp(self):
200 # For these tests we want to disable retrieving or storing credentials201 # The real keyring package should never be imported during tests.
201 # in a system-provided keyring and use a dummy keyring implementation202 assert_keyring_not_imported()
202 # instaed.203 # For these tests we want to use a dummy keyring implementation
203 self._saved_keyring = launchpadlib.credentials.keyring204 # that only stores data in memory.
204 launchpadlib.credentials.keyring = InMemoryKeyring()205 launchpadlib.credentials.keyring = InMemoryKeyring()
205206
206 def tearDown(self):207 def tearDown(self):
207 # Restore the gnomekeyring module that we disabled in setUp.208 # Remove the fake keyring module we injected during setUp.
208 launchpadlib.credentials.keyring = self._saved_keyring209 del launchpadlib.credentials.keyring
209210
210211
211class TestLaunchpadLoginWith(KeyringTest):212class TestLaunchpadLoginWith(KeyringTest):

Subscribers

People subscribed via source and target branches