diff --git a/changelog/815.feature.rst b/changelog/815.feature.rst new file mode 100644 index 00000000..aa819df6 --- /dev/null +++ b/changelog/815.feature.rst @@ -0,0 +1 @@ +Show more helpful messages for invalid passwords. diff --git a/docs/index.rst b/docs/index.rst index 9b2ae695..ead6d7d8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -88,6 +88,20 @@ Using Twine 4. Done! +.. _entering-credentials: + +.. note:: + + Like many other command line tools, Twine does not show any characters when + you enter your password. + + If you're using Windows and trying to paste your username, password, or + token in the Command Prompt or PowerShell, ``Ctrl-V`` and ``Shift+Insert`` + won't work. Instead, you can use "Edit > Paste" from the window menu, or + enable "Use Ctrl+Shift+C/V as Copy/Paste" in "Properties". This is a + `known issue `_ with Python's + ``getpass`` module. + More documentation on using Twine to upload packages to PyPI is in the `Python Packaging User Guide`_. 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 6256ee0a..0aa5c75a 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 @@ -240,11 +241,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?") + logger.warning( + " See https://twine.readthedocs.io/#entering-credentials " + "for more information." + ) + + return value + else: return None