Skip to content

Commit

Permalink
Add cli flag --force-keyring.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Dos Moonen committed Apr 18, 2022
1 parent 46bf998 commit 0c4e786
Show file tree
Hide file tree
Showing 3 changed files with 36 additions and 11 deletions.
14 changes: 14 additions & 0 deletions src/pip/_internal/cli/cmdoptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,19 @@ class PipOption(Option):
help="Disable prompting for input.",
)

force_keyring: Callable[..., Option] = partial(
Option,
"--force-keyring",
dest="force_keyring",
action="store_true",
default=False,
help=(
"Always query the keyring, regardless of pip's --no-input option. Note"
" that this may cause problems if the keyring expects to be able to"
" prompt the user interactively and no interactive user is available."
),
)

proxy: Callable[..., Option] = partial(
Option,
"--proxy",
Expand Down Expand Up @@ -993,6 +1006,7 @@ def check_list_path_option(options: Values) -> None:
quiet,
log,
no_input,
force_keyring,
proxy,
retries,
timeout,
Expand Down
4 changes: 4 additions & 0 deletions src/pip/_internal/cli/req_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ def _build_session(

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

return session

Expand Down
29 changes: 18 additions & 11 deletions src/pip/_internal/network/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,14 @@ def get_keyring_auth(url: Optional[str], username: Optional[str]) -> Optional[Au

class MultiDomainBasicAuth(AuthBase):
def __init__(
self, prompting: bool = True, index_urls: Optional[List[str]] = None
self,
prompting: bool = True,
index_urls: Optional[List[str]] = None,
use_keyring: bool = True,
) -> None:
self.prompting = prompting
self.index_urls = index_urls
self.use_keyring = keyring and use_keyring
self.passwords: Dict[str, AuthInfo] = {}
# When the user is prompted to enter credentials and keyring is
# available, we will offer to save them. If the user accepts,
Expand Down Expand Up @@ -231,7 +235,7 @@ def __call__(self, req: Request) -> Request:
def _prompt_for_password(
self, netloc: str
) -> Tuple[Optional[str], Optional[str], bool]:
username = ask_input(f"User for {netloc}: ")
username = ask_input(f"User for {netloc}: ") if self.prompting else None
if not username:
return None, None, False
auth = get_keyring_auth(netloc, username)
Expand All @@ -242,7 +246,7 @@ def _prompt_for_password(

# Factored out to allow for easy patching in tests
def _should_save_password_to_keyring(self) -> bool:
if not keyring:
if not self.prompting or not keyring:
return False
return ask("Save credentials to keyring [y/N]: ", ["y", "n"]) == "y"

Expand All @@ -252,19 +256,22 @@ def handle_401(self, resp: Response, **kwargs: Any) -> Response:
if resp.status_code != 401:
return resp

username, password = None, None

# Query the keyring for credentials:
if self.use_keyring:
username, password = self._get_new_credentials(
resp.url,
allow_netrc=True,
allow_keyring=True,
)

# We are not able to prompt the user so simply return the response
if not self.prompting:
if not self.prompting and not username and not password:
return resp

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

# Query the keyring for credentials:
username, password = self._get_new_credentials(
resp.url,
allow_netrc=True,
allow_keyring=True,
)

# Prompt the user for a new username and password
save = False
if not username and not password:
Expand Down

0 comments on commit 0c4e786

Please sign in to comment.