Skip to content

Commit

Permalink
Merge pull request #419 from zooba/issue-418
Browse files Browse the repository at this point in the history
Fixes #418 Support keyring.get_credential
  • Loading branch information
jaraco committed Oct 30, 2018
2 parents 79b1c2f + d5239b8 commit 89a35dc
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 6 deletions.
69 changes: 69 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,33 @@ def get_password(system, user):
assert pw == 'entered pw'


def test_get_username_and_password_keyring_overrides_prompt(monkeypatch):
import collections
Credential = collections.namedtuple('Credential', 'username password')

class MockKeyring:
@staticmethod
def get_credential(system, user):
return Credential(
'real_user',
'real_user@{system} sekure pa55word'.format(**locals())
)

@staticmethod
def get_password(system, user):
cred = MockKeyring.get_credential(system, user)
if user != cred.username:
raise RuntimeError("unexpected username")
return cred.password

monkeypatch.setitem(sys.modules, 'keyring', MockKeyring)

user = utils.get_username('system', None, {})
assert user == 'real_user'
pw = utils.get_password('system', user, None, {})
assert pw == 'real_user@system sekure pa55word'


@pytest.fixture
def keyring_missing(monkeypatch):
"""
Expand All @@ -237,11 +264,31 @@ def keyring_missing(monkeypatch):
monkeypatch.delitem(sys.modules, 'keyring', raising=False)


@pytest.fixture
def keyring_missing_get_credentials(monkeypatch):
"""
Simulate older versions of keyring that do not have the
'get_credentials' API.
"""
monkeypatch.delattr('keyring.backends.KeyringBackend',
'get_credential', raising=False)


@pytest.fixture
def entered_username(monkeypatch):
monkeypatch.setattr(utils, 'input_func', lambda prompt: 'entered user')


@pytest.fixture
def entered_password(monkeypatch):
monkeypatch.setattr(utils, 'password_prompt', lambda prompt: 'entered pw')


def test_get_username_keyring_missing_get_credentials_prompts(
entered_username, keyring_missing_get_credentials):
assert utils.get_username('system', None, {}) == 'entered user'


def test_get_password_keyring_missing_prompts(
entered_password, keyring_missing):
assert utils.get_password('system', 'user', None, {}) == 'entered pw'
Expand All @@ -261,6 +308,28 @@ def get_password(system, username):
monkeypatch.setitem(sys.modules, 'keyring', FailKeyring())


@pytest.fixture
def keyring_no_backends_get_credential(monkeypatch):
"""
Simulate that keyring has no available backends. When keyring
has no backends for the system, the backend will be a
fail.Keyring, which raises RuntimeError on get_password.
"""
class FailKeyring(object):
@staticmethod
def get_credential(system, username):
raise RuntimeError("fail!")
monkeypatch.setitem(sys.modules, 'keyring', FailKeyring())


def test_get_username_runtime_error_suppressed(
entered_username, keyring_no_backends_get_credential, recwarn):
assert utils.get_username('system', None, {}) == 'entered user'
assert len(recwarn) == 1
warning = recwarn.pop(UserWarning)
assert 'fail!' in str(warning)


def test_get_password_runtime_error_suppressed(
entered_password, keyring_no_backends, recwarn):
assert utils.get_password('system', 'user', None, {}) == 'entered pw'
Expand Down
6 changes: 5 additions & 1 deletion twine/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,11 @@ def _handle_repository_options(self, repository_name, repository_url):
)

def _handle_authentication(self, username, password):
self.username = utils.get_username(username, self.repository_config)
self.username = utils.get_username(
self.repository_config['repository'],
username,
self.repository_config
)
self.password = utils.get_password(
self.repository_config['repository'],
self.username,
Expand Down
41 changes: 36 additions & 5 deletions twine/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,23 @@ def get_userpass_value(cli_value, config, key, prompt_strategy=None):
return None


def get_username_from_keyring(system):
if 'keyring' not in sys.modules:
return

try:
getter = sys.modules['keyring'].get_credential
except AttributeError:
return None

try:
creds = getter(system, None)
if creds:
return creds.username
except Exception as exc:
warnings.warn(str(exc))


def password_prompt(prompt_text): # Always expects unicode for our own sanity
prompt = prompt_text
# Workaround for https://github.com/pypa/twine/issues/116
Expand All @@ -221,18 +238,32 @@ def get_password_from_keyring(system, username):
warnings.warn(str(exc))


def username_from_keyring_or_prompt(system):
return (
get_username_from_keyring(system)
or input_func('Enter your username: ')
)


def password_from_keyring_or_prompt(system, username):
return (
get_password_from_keyring(system, username)
or password_prompt('Enter your password: ')
)


get_username = functools.partial(
get_userpass_value,
key='username',
prompt_strategy=functools.partial(input_func, 'Enter your username: '),
)
def get_username(system, cli_value, config):
return get_userpass_value(
cli_value,
config,
key='username',
prompt_strategy=functools.partial(
username_from_keyring_or_prompt,
system,
),
)


get_cacert = functools.partial(
get_userpass_value,
key='ca_cert',
Expand Down

0 comments on commit 89a35dc

Please sign in to comment.