Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

twine: use API tokens by default on PyPI #1040

Merged
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion tests/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ def get_password(system, username):
)


def test_logs_cli_values(caplog):
def test_logs_cli_values(caplog, config):
caplog.set_level(logging.INFO, "twine")

res = auth.Resolver(config, auth.CredentialInput("username", "password"))
Expand Down
4 changes: 2 additions & 2 deletions tests/test_register.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,13 @@ def none_register(*args, **settings_kwargs):
replaced_register = pretend.call_recorder(none_register)
monkeypatch.setattr(register, "register", replaced_register)
testenv = {
"TWINE_USERNAME": "pypiuser",
"TWINE_USERNAME": "this-is-ignored",
"TWINE_PASSWORD": "pypipassword",
"TWINE_CERT": "/foo/bar.crt",
}
with helpers.set_env(**testenv):
cli.dispatch(["register", helpers.WHEEL_FIXTURE])
register_settings = replaced_register.calls[0].args[0]
assert "pypipassword" == register_settings.password
assert "pypiuser" == register_settings.username
assert "__token__" == register_settings.username
assert "/foo/bar.crt" == register_settings.cacert
4 changes: 2 additions & 2 deletions tests/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def test_settings_transforms_repository_config(write_config_file):
"""
[pypi]
repository: https://upload.pypi.org/legacy/
username:username
username:this-is-ignored
password:password
"""
)
Expand All @@ -43,7 +43,7 @@ def test_settings_transforms_repository_config(write_config_file):
assert s.sign is False
assert s.sign_with == "gpg"
assert s.identity is None
assert s.username == "username"
assert s.username == "__token__"
assert s.password == "password"
assert s.cacert is None
assert s.client_cert is None
Expand Down
4 changes: 2 additions & 2 deletions tests/test_upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -551,15 +551,15 @@ def none_upload(*args, **settings_kwargs):
replaced_upload = pretend.call_recorder(none_upload)
monkeypatch.setattr(upload, "upload", replaced_upload)
testenv = {
"TWINE_USERNAME": "pypiuser",
"TWINE_USERNAME": "this-is-ignored",
"TWINE_PASSWORD": "pypipassword",
"TWINE_CERT": "/foo/bar.crt",
}
with helpers.set_env(**testenv):
cli.dispatch(["upload", "path/to/file"])
upload_settings = replaced_upload.calls[0].args[0]
assert "pypipassword" == upload_settings.password
assert "pypiuser" == upload_settings.username
assert "__token__" == upload_settings.username
assert "/foo/bar.crt" == upload_settings.cacert


Expand Down
14 changes: 13 additions & 1 deletion twine/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ 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):
# As of 2024-01-01, PyPI requires API tokens for uploads, meaning
# that the username is invariant.
return "__token__"

return utils.get_userpass_value(
self.input.username,
self.config,
Expand Down Expand Up @@ -90,7 +95,14 @@ def password_from_keyring_or_prompt(self) -> str:
logger.info("password set from keyring")
return password

return self.prompt("password", getpass.getpass)
# 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):
prompt = "API token"
else:
prompt = "password"

return self.prompt(prompt, getpass.getpass)

def prompt(self, what: str, how: Callable[..., str]) -> str:
return how(f"Enter your {what}: ")
Expand Down
3 changes: 0 additions & 3 deletions twine/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,9 +295,6 @@ def _handle_repository_options(
repository_name,
repository_url,
)
self.repository_config["repository"] = utils.normalize_repository_url(
cast(str, self.repository_config["repository"]),
)

def _handle_certificates(
self, cacert: Optional[str], client_cert: Optional[str]
Expand Down
7 changes: 5 additions & 2 deletions twine/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import os
import os.path
import unicodedata
from typing import Any, Callable, DefaultDict, Dict, Optional, Sequence, Union
from typing import Any, Callable, DefaultDict, Dict, Optional, Sequence, Union, cast
from urllib.parse import urlparse
from urllib.parse import urlunparse

Expand Down Expand Up @@ -133,7 +133,7 @@ def get_repository_from_config(
}

try:
return get_config(config_file)[repository]
config = get_config(config_file)[repository]
except OSError as exc:
raise exceptions.InvalidConfiguration(str(exc))
except KeyError:
Expand All @@ -142,6 +142,9 @@ def get_repository_from_config(
f"More info: https://packaging.python.org/specifications/pypirc/ "
)

config["repository"] = normalize_repository_url(cast(str, config["repository"]))
return config


_HOSTNAMES = {
"pypi.python.org",
Expand Down