diff --git a/changelog/1121.removal.rst b/changelog/1121.removal.rst new file mode 100644 index 00000000..36c5dd10 --- /dev/null +++ b/changelog/1121.removal.rst @@ -0,0 +1 @@ +Username for PyPI and Test PyPI now defaults to __token__ but no longer overrides a username configured in the environment or supplied on the command line. Workflows still supplying anything other than __token__ for the username when uploading to PyPI or Test PyPI will now fail. Either supply __token__ or do not supply a username at all. \ No newline at end of file diff --git a/tests/test_register.py b/tests/test_register.py index cc2ae71e..eb20e0ed 100644 --- a/tests/test_register.py +++ b/tests/test_register.py @@ -90,8 +90,7 @@ def none_register(*args, **settings_kwargs): monkeypatch.setattr(register, "register", replaced_register) testenv = { "TWINE_REPOSITORY": repo, - # Ignored because the TWINE_REPOSITORY is PyPI/TestPyPI - "TWINE_USERNAME": "this-is-ignored", + "TWINE_USERNAME": "pypiuser", "TWINE_PASSWORD": "pypipassword", "TWINE_CERT": "/foo/bar.crt", } @@ -99,7 +98,7 @@ def none_register(*args, **settings_kwargs): cli.dispatch(["register", helpers.WHEEL_FIXTURE]) register_settings = replaced_register.calls[0].args[0] assert "pypipassword" == register_settings.password - assert "__token__" == register_settings.username + assert "pypiuser" == register_settings.username assert "/foo/bar.crt" == register_settings.cacert diff --git a/tests/test_upload.py b/tests/test_upload.py index 0bce83f2..4dedfda4 100644 --- a/tests/test_upload.py +++ b/tests/test_upload.py @@ -604,8 +604,7 @@ def none_upload(*args, **settings_kwargs): monkeypatch.setattr(upload, "upload", replaced_upload) testenv = { "TWINE_REPOSITORY": repo, - # Ignored because TWINE_REPOSITORY is PyPI/TestPyPI - "TWINE_USERNAME": "this-is-ignored", + "TWINE_USERNAME": "pypiuser", "TWINE_PASSWORD": "pypipassword", "TWINE_CERT": "/foo/bar.crt", } @@ -613,7 +612,7 @@ def none_upload(*args, **settings_kwargs): cli.dispatch(["upload", "path/to/file"]) upload_settings = replaced_upload.calls[0].args[0] assert "pypipassword" == upload_settings.password - assert "__token__" == upload_settings.username + assert "pypiuser" == upload_settings.username assert "/foo/bar.crt" == upload_settings.cacert diff --git a/twine/auth.py b/twine/auth.py index 2d3e2604..cbb2ccd4 100644 --- a/twine/auth.py +++ b/twine/auth.py @@ -31,12 +31,9 @@ def choose(cls, interactive: bool) -> Type["Resolver"]: @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__" + if self.is_pypi() and not self.input.username: + # Default username. + self.input.username = "__token__" return utils.get_userpass_value( self.input.username, @@ -97,20 +94,23 @@ def password_from_keyring_or_prompt(self) -> str: 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" + # Prompt for API token when required. + what = "API token" if self.is_pypi() else "password" - return self.prompt(prompt, getpass.getpass) + return self.prompt(what, getpass.getpass) def prompt(self, what: str, how: Callable[..., str]) -> str: return how(f"Enter your {what}: ") + def is_pypi(self) -> bool: + """As of 2024-01-01, PyPI requires API tokens for uploads.""" + return cast(str, self.config["repository"]).startswith( + ( + utils.DEFAULT_REPOSITORY, + utils.TEST_REPOSITORY, + ) + ) + class Private(Resolver): def prompt(self, what: str, how: Optional[Callable[..., str]] = None) -> str: