3
3
Contains interface (MultiDomainBasicAuth) and associated glue code for
4
4
providing credentials in the context of network requests.
5
5
"""
6
-
6
+ import functools
7
7
import os
8
8
import shutil
9
9
import subprocess
10
+ import sys
11
+ import sysconfig
12
+ import typing
10
13
import urllib .parse
11
14
from abc import ABC , abstractmethod
15
+ from functools import lru_cache
16
+ from pathlib import Path
12
17
from typing import Any , Dict , List , NamedTuple , Optional , Tuple
13
18
14
19
from pip ._vendor .requests .auth import AuthBase , HTTPBasicAuth
@@ -123,7 +128,8 @@ def _get_password(self, service_name: str, username: str) -> Optional[str]:
123
128
res = subprocess .run (
124
129
cmd ,
125
130
stdin = subprocess .DEVNULL ,
126
- capture_output = True ,
131
+ stdout = subprocess .PIPE ,
132
+ stderr = sys .stderr ,
127
133
env = env ,
128
134
)
129
135
if res .returncode :
@@ -144,12 +150,19 @@ def _set_password(self, service_name: str, username: str, password: str) -> None
144
150
return None
145
151
146
152
147
- def get_keyring_provider () -> KeyRingBaseProvider :
153
+ @lru_cache (maxsize = None )
154
+ def get_keyring_provider (provider : str ) -> KeyRingBaseProvider :
155
+ logger .verbose ("Keyring provider requested: %s" , provider )
156
+
148
157
# keyring has previously failed and been disabled
149
- if not KEYRING_DISABLED :
150
- # Default to trying to use Python provider
158
+ if KEYRING_DISABLED or provider == "disabled" :
159
+ pass
160
+ elif provider == "import" :
151
161
try :
152
- return KeyRingPythonProvider ()
162
+ try :
163
+ return KeyRingPythonProvider ()
164
+ finally :
165
+ logger .verbose ("Keyring provider set: import" )
153
166
except ImportError :
154
167
pass
155
168
except Exception as exc :
@@ -160,22 +173,54 @@ def get_keyring_provider() -> KeyRingBaseProvider:
160
173
"trying to find a keyring executable as a fallback" ,
161
174
str (exc ),
162
175
)
163
-
164
- # Fallback to Cli Provider if `keyring` isn't installed
176
+ elif provider == "subprocess" :
165
177
cli = shutil .which ("keyring" )
178
+ if cli and cli .startswith (sysconfig .get_path ("scripts" )):
179
+ # all code within this function is stolen from shutil.which implementation
180
+ @typing .no_type_check
181
+ def PATH_as_shutil_which_determines_it () -> str :
182
+ path = os .environ .get ("PATH" , None )
183
+ if path is None :
184
+ try :
185
+ path = os .confstr ("CS_PATH" )
186
+ except (AttributeError , ValueError ):
187
+ # os.confstr() or CS_PATH is not available
188
+ path = os .defpath
189
+ # bpo-35755: Don't use os.defpath if the PATH environment variable is
190
+ # set to an empty string
191
+
192
+ return path
193
+
194
+ scripts = Path (sysconfig .get_path ("scripts" )).resolve ()
195
+
196
+ paths = []
197
+ for path in PATH_as_shutil_which_determines_it ().split (os .pathsep ):
198
+ p = Path (path )
199
+ if p .exists () and not p .resolve ().samefile (scripts ):
200
+ paths .append (path )
201
+
202
+ path = os .pathsep .join (paths )
203
+
204
+ cli = shutil .which ("keyring" , path = path )
205
+
166
206
if cli :
207
+ logger .verbose ("Keyring provider set: subprocess with executable %s" , cli )
167
208
return KeyRingCliProvider (cli )
209
+ else :
210
+ logger .verbose ("Unknown keyring provider requested: %s" , provider )
168
211
212
+ logger .verbose ("Keyring provider set: disabled" )
169
213
return KeyRingNullProvider ()
170
214
171
215
172
- def get_keyring_auth (url : Optional [str ], username : Optional [str ]) -> Optional [AuthInfo ]:
216
+ def get_keyring_auth (
217
+ keyring : KeyRingBaseProvider , url : Optional [str ], username : Optional [str ]
218
+ ) -> Optional [AuthInfo ]:
173
219
"""Return the tuple auth for a given url from keyring."""
174
220
# Do nothing if no url was provided
175
221
if not url :
176
222
return None
177
223
178
- keyring = get_keyring_provider ()
179
224
try :
180
225
return keyring .get_auth_info (url , username )
181
226
except Exception as exc :
@@ -185,6 +230,7 @@ def get_keyring_auth(url: Optional[str], username: Optional[str]) -> Optional[Au
185
230
)
186
231
global KEYRING_DISABLED
187
232
KEYRING_DISABLED = True
233
+ get_keyring_provider .cache_clear ()
188
234
return None
189
235
190
236
@@ -193,10 +239,12 @@ def __init__(
193
239
self ,
194
240
prompting : bool = True ,
195
241
index_urls : Optional [List [str ]] = None ,
242
+ keyring_provider : str = "disabled" ,
196
243
use_keyring : bool = True ,
197
244
) -> None :
198
245
self .prompting = prompting
199
246
self .index_urls = index_urls
247
+ self .keyring_provider = keyring_provider # type: ignore[assignment]
200
248
self .use_keyring = use_keyring
201
249
self .passwords : Dict [str , AuthInfo ] = {}
202
250
# When the user is prompted to enter credentials and keyring is
@@ -206,6 +254,18 @@ def __init__(
206
254
# ``save_credentials`` to save these.
207
255
self ._credentials_to_save : Optional [Credentials ] = None
208
256
257
+ @property
258
+ def keyring_provider (self ) -> KeyRingBaseProvider :
259
+ return self ._keyring_provider ()
260
+
261
+ @keyring_provider .setter
262
+ def keyring_provider (self , provider : str ) -> None :
263
+ # The free function get_keyring_provider has been decorated with
264
+ # functools.cache. If an exception occurs in get_keyring_auth that
265
+ # cache will be cleared and keyring disabled, take that into account
266
+ # if you want to remove this indirection.
267
+ self ._keyring_provider = functools .partial (get_keyring_provider , provider )
268
+
209
269
def _get_index_url (self , url : str ) -> Optional [str ]:
210
270
"""Return the original index URL matching the requested URL.
211
271
@@ -275,8 +335,8 @@ def _get_new_credentials(
275
335
# The index url is more specific than the netloc, so try it first
276
336
# fmt: off
277
337
kr_auth = (
278
- get_keyring_auth (index_url , username ) or
279
- get_keyring_auth (netloc , username )
338
+ get_keyring_auth (self . keyring_provider , index_url , username ) or
339
+ get_keyring_auth (self . keyring_provider , netloc , username )
280
340
)
281
341
# fmt: on
282
342
if kr_auth :
@@ -356,15 +416,15 @@ def _prompt_for_password(
356
416
username = ask_input (f"User for { netloc } : " ) if self .prompting else None
357
417
if not username :
358
418
return None , None , False
359
- auth = get_keyring_auth (netloc , username )
419
+ auth = get_keyring_auth (self . keyring_provider , netloc , username )
360
420
if auth and auth [0 ] is not None and auth [1 ] is not None :
361
421
return auth [0 ], auth [1 ], False
362
422
password = ask_password ("Password: " )
363
423
return username , password , True
364
424
365
425
# Factored out to allow for easy patching in tests
366
426
def _should_save_password_to_keyring (self ) -> bool :
367
- if not self .prompting or get_keyring_provider () is None :
427
+ if not self .prompting or isinstance ( self . keyring_provider , KeyRingNullProvider ) :
368
428
return False
369
429
return ask ("Save credentials to keyring [y/N]: " , ["y" , "n" ]) == "y"
370
430
@@ -439,16 +499,17 @@ def warn_on_401(self, resp: Response, **kwargs: Any) -> None:
439
499
440
500
def save_credentials (self , resp : Response , ** kwargs : Any ) -> None :
441
501
"""Response callback to save credentials on success."""
442
- keyring = get_keyring_provider ()
443
502
assert not isinstance (
444
- keyring , KeyRingNullProvider
503
+ self . keyring_provider , KeyRingNullProvider
445
504
), "should never reach here without keyring"
446
505
447
506
creds = self ._credentials_to_save
448
507
self ._credentials_to_save = None
449
508
if creds and resp .status_code < 400 :
450
509
try :
451
510
logger .info ("Saving credentials to keyring" )
452
- keyring .save_auth_info (creds .url , creds .username , creds .password )
511
+ self .keyring_provider .save_auth_info (
512
+ creds .url , creds .username , creds .password
513
+ )
453
514
except Exception :
454
515
logger .exception ("Failed to save credentials" )
0 commit comments