Skip to content

Commit 497761c

Browse files
author
Dos Moonen
committed
Add cli flag --force-keyring.
Currently, Pip is conservative and does not query keyring at all when `--no-input` is used because the keyring might require user interaction such as prompting the user on the console. This commit adds a flag to force keyring usage if it is installed. It defaults to `False`, making this opt-in behaviour. Tools such as Pipx and Pipenv use Pip and have their own progress indicator that hides output from the subprocess Pip runs in. They should pass `--no-input` in my opinion, but Pip should provide some mechanism to still query the keyring in that case. Just not by default. So here we are.
1 parent 51475ec commit 497761c

File tree

3 files changed

+40
-14
lines changed

3 files changed

+40
-14
lines changed

src/pip/_internal/cli/cmdoptions.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,19 @@ class PipOption(Option):
244244
help="Disable prompting for input.",
245245
)
246246

247+
force_keyring: Callable[..., Option] = partial(
248+
Option,
249+
"--force-keyring",
250+
dest="force_keyring",
251+
action="store_true",
252+
default=False,
253+
help=(
254+
"Always query the keyring, regardless of pip's --no-input option. Note"
255+
" that this may cause problems if the keyring expects to be able to"
256+
" prompt the user interactively and no interactive user is available."
257+
),
258+
)
259+
247260
proxy: Callable[..., Option] = partial(
248261
Option,
249262
"--proxy",
@@ -1019,6 +1032,7 @@ def check_list_path_option(options: Values) -> None:
10191032
quiet,
10201033
log,
10211034
no_input,
1035+
force_keyring,
10221036
proxy,
10231037
retries,
10241038
timeout,

src/pip/_internal/cli/req_command.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,10 @@ def _build_session(
151151

152152
# Determine if we can prompt the user for authentication or not
153153
session.auth.prompting = not options.no_input
154+
# We won't use keyring when --no-input is passed unless
155+
# --force-keyring is passed as well because it might require
156+
# user interaction
157+
session.auth.use_keyring = session.auth.prompting or options.force_keyring
154158

155159
return session
156160

src/pip/_internal/network/auth.py

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -128,15 +128,15 @@ def _get_password(self, service_name: str, username: str) -> Optional[str]:
128128
)
129129
if res.returncode:
130130
return None
131-
return res.stdout.decode("utf-8").strip("\n")
131+
return res.stdout.decode("utf-8").strip(os.linesep)
132132

133133
def _set_password(self, service_name: str, username: str, password: str) -> None:
134134
"""Mirror the implementation of keyring.set_password using cli"""
135135
if self.keyring is None:
136136
return None
137137

138138
cmd = [self.keyring, "set", service_name, username]
139-
input_ = password.encode("utf-8") + b"\n"
139+
input_ = (password + os.linesep).encode("utf-8")
140140
env = os.environ.copy()
141141
env["PYTHONIOENCODING"] = "utf-8"
142142
res = subprocess.run(cmd, input=input_, env=env)
@@ -190,10 +190,14 @@ def get_keyring_auth(url: Optional[str], username: Optional[str]) -> Optional[Au
190190

191191
class MultiDomainBasicAuth(AuthBase):
192192
def __init__(
193-
self, prompting: bool = True, index_urls: Optional[List[str]] = None
193+
self,
194+
prompting: bool = True,
195+
index_urls: Optional[List[str]] = None,
196+
use_keyring: bool = True,
194197
) -> None:
195198
self.prompting = prompting
196199
self.index_urls = index_urls
200+
self.use_keyring = use_keyring
197201
self.passwords: Dict[str, AuthInfo] = {}
198202
# When the user is prompted to enter credentials and keyring is
199203
# available, we will offer to save them. If the user accepts,
@@ -227,7 +231,8 @@ def _get_index_url(self, url: str) -> Optional[str]:
227231
def _get_new_credentials(
228232
self,
229233
original_url: str,
230-
allow_netrc: bool = True,
234+
*,
235+
allow_netrc: bool = False,
231236
allow_keyring: bool = False,
232237
) -> AuthInfo:
233238
"""Find and return credentials for the specified URL."""
@@ -348,7 +353,7 @@ def __call__(self, req: Request) -> Request:
348353
def _prompt_for_password(
349354
self, netloc: str
350355
) -> Tuple[Optional[str], Optional[str], bool]:
351-
username = ask_input(f"User for {netloc}: ")
356+
username = ask_input(f"User for {netloc}: ") if self.prompting else None
352357
if not username:
353358
return None, None, False
354359
auth = get_keyring_auth(netloc, username)
@@ -359,7 +364,7 @@ def _prompt_for_password(
359364

360365
# Factored out to allow for easy patching in tests
361366
def _should_save_password_to_keyring(self) -> bool:
362-
if get_keyring_provider() is None:
367+
if not self.prompting or get_keyring_provider() is None:
363368
return False
364369
return ask("Save credentials to keyring [y/N]: ", ["y", "n"]) == "y"
365370

@@ -369,19 +374,22 @@ def handle_401(self, resp: Response, **kwargs: Any) -> Response:
369374
if resp.status_code != 401:
370375
return resp
371376

377+
username, password = None, None
378+
379+
# Query the keyring for credentials:
380+
if self.use_keyring:
381+
username, password = self._get_new_credentials(
382+
resp.url,
383+
allow_netrc=False,
384+
allow_keyring=True,
385+
)
386+
372387
# We are not able to prompt the user so simply return the response
373-
if not self.prompting:
388+
if not self.prompting and not username and not password:
374389
return resp
375390

376391
parsed = urllib.parse.urlparse(resp.url)
377392

378-
# Query the keyring for credentials:
379-
username, password = self._get_new_credentials(
380-
resp.url,
381-
allow_netrc=False,
382-
allow_keyring=True,
383-
)
384-
385393
# Prompt the user for a new username and password
386394
save = False
387395
if not username and not password:

0 commit comments

Comments
 (0)