diff --git a/tests/test_utils.py b/tests/test_utils.py index 520b78ff..68d393dd 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -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): """ @@ -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' @@ -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' diff --git a/twine/settings.py b/twine/settings.py index 8e270fdb..d4e3d1a7 100644 --- a/twine/settings.py +++ b/twine/settings.py @@ -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, diff --git a/twine/utils.py b/twine/utils.py index 57eefb3c..a4f64196 100644 --- a/twine/utils.py +++ b/twine/utils.py @@ -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 @@ -221,6 +238,13 @@ 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) @@ -228,11 +252,18 @@ def password_from_keyring_or_prompt(system, username): ) -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',