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
1=== modified file 'src/launchpadlib/credentials.py'
2--- src/launchpadlib/credentials.py 2011-01-05 13:31:26 +0000
3+++ src/launchpadlib/credentials.py 2011-01-10 16:23:59 +0000
4@@ -37,7 +37,6 @@
5 from urlparse import urljoin
6 import webbrowser
7
8-import keyring
9 import simplejson
10
11 from lazr.restfulclient.errors import HTTPError
12@@ -295,13 +294,28 @@
13 integrating third-party websites into Launchpad.
14 """
15
16+ @staticmethod
17+ def _ensure_keyring_imported():
18+ """Ensure the keyring module is imported (postponing side effects).
19+
20+ The keyring module initializes the environment-dependent backend at
21+ import time (nasty). We want to avoid that initialization because it
22+ may do things like prompt the user to unlock their password store
23+ (e.g., KWallet).
24+ """
25+ if 'keyring' not in globals():
26+ global keyring
27+ import keyring
28+
29 def do_save(self, credentials, unique_key):
30 """Store newly-authorized credentials in the keyring."""
31+ self._ensure_keyring_imported()
32 keyring.set_password(
33 'launchpadlib', unique_key, credentials.serialize())
34
35 def do_load(self, unique_key):
36 """Retrieve credentials from the keyring."""
37+ self._ensure_keyring_imported()
38 credential_string = keyring.get_password(
39 'launchpadlib', unique_key)
40 if credential_string is not None:
41
42=== modified file 'src/launchpadlib/testing/helpers.py'
43--- src/launchpadlib/testing/helpers.py 2011-01-04 20:00:37 +0000
44+++ src/launchpadlib/testing/helpers.py 2011-01-10 16:23:59 +0000
45@@ -44,6 +44,14 @@
46 )
47
48
49+missing = object()
50+
51+
52+def assert_keyring_not_imported():
53+ assert getattr(launchpadlib.credentials, 'keyring', missing) is missing, (
54+ 'During tests the real keyring module should never be imported.')
55+
56+
57 class NoNetworkAuthorizationEngine(RequestTokenAuthorizationEngine):
58 """An authorization engine that doesn't open a web browser.
59
60@@ -121,12 +129,14 @@
61
62 @contextmanager
63 def fake_keyring(fake):
64- original_keyring = launchpadlib.credentials.keyring
65+ """A context manager which injects a testing keyring implementation."""
66+ # The real keyring package should never be imported during tests.
67+ assert_keyring_not_imported()
68 launchpadlib.credentials.keyring = fake
69 try:
70 yield
71 finally:
72- launchpadlib.credentials.keyring = original_keyring
73+ del launchpadlib.credentials.keyring
74
75
76 class FauxSocketModule:
77
78=== modified file 'src/launchpadlib/tests/test_http.py'
79--- src/launchpadlib/tests/test_http.py 2011-01-04 18:46:27 +0000
80+++ src/launchpadlib/tests/test_http.py 2011-01-10 16:23:59 +0000
81@@ -17,11 +17,13 @@
82 """Tests for the LaunchpadOAuthAwareHTTP class."""
83
84 from collections import deque
85+import tempfile
86 import unittest
87
88 from simplejson import dumps, JSONDecodeError
89
90 from launchpadlib.errors import Unauthorized
91+from launchpadlib.credentials import UnencryptedFileCredentialStore
92 from launchpadlib.launchpad import (
93 Launchpad,
94 LaunchpadOAuthAwareHttp,
95@@ -92,6 +94,11 @@
96 return SimulatedResponsesHttp(
97 deque(self.responses), self, self.authorization_engine, *args)
98
99+ @classmethod
100+ def credential_store_factory(cls, credential_save_failed):
101+ return UnencryptedFileCredentialStore(
102+ tempfile.mkstemp()[1], credential_save_failed)
103+
104
105 class SimulatedResponsesTestCase(unittest.TestCase):
106 """Test cases that give fake responses to launchpad's HTTP requests."""
107@@ -166,7 +173,7 @@
108 self.assertEquals(self.engine.access_tokens_obtained, 0)
109 launchpad = SimulatedResponsesLaunchpad.login_with(
110 'application name', authorization_engine=self.engine)
111- self.assertEquals(self.engine.access_tokens_obtained, 0)
112+ self.assertEquals(self.engine.access_tokens_obtained, 1)
113
114 def test_bad_token(self):
115 """If our token is bad, we get another one."""
116@@ -178,7 +185,7 @@
117 self.assertEquals(self.engine.access_tokens_obtained, 0)
118 launchpad = SimulatedResponsesLaunchpad.login_with(
119 'application name', authorization_engine=self.engine)
120- self.assertEquals(self.engine.access_tokens_obtained, 1)
121+ self.assertEquals(self.engine.access_tokens_obtained, 2)
122
123 def test_expired_token(self):
124 """If our token is expired, we get another one."""
125@@ -191,7 +198,7 @@
126 self.assertEquals(self.engine.access_tokens_obtained, 0)
127 launchpad = SimulatedResponsesLaunchpad.login_with(
128 'application name', authorization_engine=self.engine)
129- self.assertEquals(self.engine.access_tokens_obtained, 1)
130+ self.assertEquals(self.engine.access_tokens_obtained, 2)
131
132 def test_delayed_error(self):
133 """We get another token no matter when the error happens."""
134@@ -203,7 +210,7 @@
135 self.assertEquals(self.engine.access_tokens_obtained, 0)
136 launchpad = SimulatedResponsesLaunchpad.login_with(
137 'application name', authorization_engine=self.engine)
138- self.assertEquals(self.engine.access_tokens_obtained, 1)
139+ self.assertEquals(self.engine.access_tokens_obtained, 2)
140
141 def test_many_errors(self):
142 """We'll keep getting new tokens as long as tokens are the problem."""
143@@ -216,7 +223,7 @@
144 self.assertEquals(self.engine.access_tokens_obtained, 0)
145 launchpad = SimulatedResponsesLaunchpad.login_with(
146 'application name', authorization_engine=self.engine)
147- self.assertEquals(self.engine.access_tokens_obtained, 3)
148+ self.assertEquals(self.engine.access_tokens_obtained, 4)
149
150 def test_other_unauthorized(self):
151 """If the token is not at fault, a 401 error raises an exception."""
152
153=== modified file 'src/launchpadlib/tests/test_launchpad.py'
154--- src/launchpadlib/tests/test_launchpad.py 2011-01-05 13:31:26 +0000
155+++ src/launchpadlib/tests/test_launchpad.py 2011-01-10 16:23:59 +0000
156@@ -37,6 +37,7 @@
157 import launchpadlib.launchpad
158 from launchpadlib.launchpad import Launchpad
159 from launchpadlib.testing.helpers import (
160+ assert_keyring_not_imported,
161 BadSaveKeyring,
162 fake_keyring,
163 FauxSocketModule,
164@@ -197,15 +198,15 @@
165 """Base class for tests that use the keyring."""
166
167 def setUp(self):
168- # For these tests we want to disable retrieving or storing credentials
169- # in a system-provided keyring and use a dummy keyring implementation
170- # instaed.
171- self._saved_keyring = launchpadlib.credentials.keyring
172+ # The real keyring package should never be imported during tests.
173+ assert_keyring_not_imported()
174+ # For these tests we want to use a dummy keyring implementation
175+ # that only stores data in memory.
176 launchpadlib.credentials.keyring = InMemoryKeyring()
177
178 def tearDown(self):
179- # Restore the gnomekeyring module that we disabled in setUp.
180- launchpadlib.credentials.keyring = self._saved_keyring
181+ # Remove the fake keyring module we injected during setUp.
182+ del launchpadlib.credentials.keyring
183
184
185 class TestLaunchpadLoginWith(KeyringTest):

Subscribers

People subscribed via source and target branches