import functools
import getpass
import logging
from typing import Callable, Optional, Type, cast
import keyring
from twine import exceptions
from twine import utils
logger = logging.getLogger(__name__)
[docs]class Resolver:
[docs] def __init__(self, config: utils.RepositoryConfig, input: CredentialInput) -> None:
self.config = config
self.input = input
[docs] @classmethod
def choose(cls, interactive: bool) -> Type["Resolver"]:
return cls if interactive else Private
@property
@functools.lru_cache()
def username(self) -> Optional[str]:
if cast(str, self.config["repository"]).startswith(
(utils.DEFAULT_REPOSITORY, utils.TEST_REPOSITORY)
):
# As of 2024-01-01, PyPI requires API tokens for uploads, meaning
# that the username is invariant.
return "__token__"
return utils.get_userpass_value(
self.input.username,
self.config,
key="username",
prompt_strategy=self.username_from_keyring_or_prompt,
)
@property
@functools.lru_cache()
def password(self) -> Optional[str]:
return utils.get_userpass_value(
self.input.password,
self.config,
key="password",
prompt_strategy=self.password_from_keyring_or_prompt,
)
@property
def system(self) -> Optional[str]:
return self.config["repository"]
[docs] def get_username_from_keyring(self) -> Optional[str]:
try:
system = cast(str, self.system)
logger.info("Querying keyring for username")
creds = keyring.get_credential(system, None)
if creds:
return cast(str, creds.username)
except AttributeError:
# To support keyring prior to 15.2
pass
except Exception as exc:
logger.warning("Error getting username from keyring", exc_info=exc)
return None
[docs] def get_password_from_keyring(self) -> Optional[str]:
try:
system = cast(str, self.system)
username = cast(str, self.username)
logger.info("Querying keyring for password")
return cast(str, keyring.get_password(system, username))
except Exception as exc:
logger.warning("Error getting password from keyring", exc_info=exc)
return None
[docs] def username_from_keyring_or_prompt(self) -> str:
username = self.get_username_from_keyring()
if username:
logger.info("username set from keyring")
return username
return self.prompt("username", input)
[docs] def password_from_keyring_or_prompt(self) -> str:
password = self.get_password_from_keyring()
if password:
logger.info("password set from keyring")
return password
# As of 2024-01-01, PyPI requires API tokens for uploads;
# specialize the prompt to clarify that an API token must be provided.
if cast(str, self.config["repository"]).startswith(
(utils.DEFAULT_REPOSITORY, utils.TEST_REPOSITORY)
):
prompt = "API token"
else:
prompt = "password"
return self.prompt(prompt, getpass.getpass)
[docs] def prompt(self, what: str, how: Callable[..., str]) -> str:
return how(f"Enter your {what}: ")
[docs]class Private(Resolver):
[docs] def prompt(self, what: str, how: Optional[Callable[..., str]] = None) -> str:
raise exceptions.NonInteractive(f"Credential not found for {what}.")