diff --git a/tests/test_auth.py b/tests/test_auth.py index c8e470d5..7e59b2b8 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -1,3 +1,4 @@ +import getpass import logging import pytest @@ -202,3 +203,26 @@ def test_logs_config_values(config, caplog): "username set from config file", "password set from config file", ] + + +@pytest.mark.parametrize( + "password, warning", + [ + ("", "Your password is empty"), + ("\x16", "Your password contains control characters"), + ("entered\x16pw", "Your password contains control characters"), + ], +) +def test_warns_for_empty_password( + password, + warning, + monkeypatch, + entered_username, + config, + caplog, +): + monkeypatch.setattr(getpass, "getpass", lambda prompt: password) + + assert auth.Resolver(config, auth.CredentialInput()).password == password + + assert caplog.messages[0].startswith(f" {warning}") diff --git a/twine/utils.py b/twine/utils.py index 0378f40e..bf96fc9f 100644 --- a/twine/utils.py +++ b/twine/utils.py @@ -18,6 +18,7 @@ import logging import os import os.path +import unicodedata from typing import Any, Callable, DefaultDict, Dict, Optional, Sequence, Union from urllib.parse import urlparse from urllib.parse import urlunparse @@ -237,11 +238,31 @@ def get_userpass_value( if cli_value is not None: logger.info(f"{key} set by command options") return cli_value + elif config.get(key) is not None: logger.info(f"{key} set from config file") return config[key] + elif prompt_strategy: - return prompt_strategy() + warning = "" + value = prompt_strategy() + + if not value: + warning = f"Your {key} is empty" + elif any(unicodedata.category(c).startswith("C") for c in value): + # See https://www.unicode.org/reports/tr44/#General_Category_Values + # Most common case is "\x16" when pasting in Windows Command Prompt + warning = f"Your {key} contains control characters" + + if warning: + logger.warning(f" {warning}. Did you enter it correctly?") + # TODO: Link to new entry in Twine docs + logger.warning( + " See https://pypi.org/help/#invalid-auth for more information." + ) + + return value + else: return None