From 38e0c7a476133ab565e25dcdadb5e68114b0af59 Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Sun, 6 Oct 2024 07:47:10 +0200 Subject: [PATCH] Make keyring optional on ppc64le and s390x Twine can now be installed on Linux pp64le and s390x without C and Rust compiler. The `keyring` package has an indirect dependency on `cryptography` package. Upstream PyCA does not provide wheels for ppc64le and s390x architecture, because the team has no means to run CI in reasonable time. Fixes: 1158 Signed-off-by: Christian Heimes --- .coveragerc | 3 +++ pyproject.toml | 6 +++++- tests/test_auth.py | 27 +++++++++++++++++++++++++++ twine/auth.py | 20 +++++++++++++++++--- 4 files changed, 52 insertions(+), 4 deletions(-) diff --git a/.coveragerc b/.coveragerc index c43da0a4..62ada8f6 100644 --- a/.coveragerc +++ b/.coveragerc @@ -11,5 +11,8 @@ exclude_lines = # Don't complain if non-runnable code isn't run if __name__ == .__main__.: + # exclude typing.TYPE_CHECKING + if TYPE_CHECKING: + [html] show_contexts = True diff --git a/pyproject.toml b/pyproject.toml index 1a3f1bbc..52c3de43 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,8 @@ dependencies = [ "requests-toolbelt >= 0.8.0, != 0.9.0", "urllib3 >= 1.26.0", "importlib-metadata >= 3.6", - "keyring >= 15.1", + # workaround for missing binaries on these platforms, see #1158 + "keyring >= 15.1; platform_machine != 'ppc64le' and platform_machine != 's390x'", "rfc3986 >= 1.4.0", "rich >= 12.0.0", @@ -60,6 +61,9 @@ check = "twine.commands.check:main" upload = "twine.commands.upload:main" register = "twine.commands.register:main" +[project.optional-dependencies] +keyring = ["keyring >= 15.1"] + [project.scripts] twine = "twine.__main__:main" diff --git a/tests/test_auth.py b/tests/test_auth.py index e0626d7b..1750f633 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -1,5 +1,6 @@ import getpass import logging +import platform import re import pytest @@ -26,6 +27,15 @@ def get_credential(system, user): assert username == "entered user" +def test_get_username_keyring_not_installed_defers_to_prompt( + monkeypatch, entered_username, config +): + monkeypatch.setattr(auth, "keyring", None) + + username = auth.Resolver(config, auth.CredentialInput()).username + assert username == "entered user" + + def test_get_password_keyring_defers_to_prompt(monkeypatch, entered_password, config): class MockKeyring: @staticmethod @@ -38,6 +48,15 @@ def get_password(system, user): assert pw == "entered pw" +def test_get_password_keyring_not_installed_defers_to_prompt( + monkeypatch, entered_password, config +): + monkeypatch.setattr(auth, "keyring", None) + + pw = auth.Resolver(config, auth.CredentialInput("user")).password + assert pw == "entered pw" + + def test_no_password_defers_to_prompt(monkeypatch, entered_password, config): config.update(password=None) pw = auth.Resolver(config, auth.CredentialInput("user")).password @@ -272,3 +291,11 @@ def test_warns_for_empty_password( assert auth.Resolver(config, auth.CredentialInput()).password == password assert caplog.messages[0].startswith(warning) + + +@pytest.mark.skipif( + platform.machine() in {"ppc64le", "s390x"}, + reason="keyring module is optional on ppc64le and s390x", +) +def test_keyring_module(): + assert auth.keyring is not None diff --git a/twine/auth.py b/twine/auth.py index 2d3e2604..d8b78e62 100644 --- a/twine/auth.py +++ b/twine/auth.py @@ -1,9 +1,17 @@ import functools import getpass import logging -from typing import Callable, Optional, Type, cast - -import keyring +from typing import TYPE_CHECKING, Callable, Optional, Type, cast + +# keyring has an indirect dependency on PyCA cryptography, which has no +# pre-built wheels for ppc64le and s390x, see #1158. +if TYPE_CHECKING: + import keyring +else: + try: + import keyring + except ModuleNotFoundError: # pragma: no cover + keyring = None from twine import exceptions from twine import utils @@ -60,6 +68,9 @@ def system(self) -> Optional[str]: return self.config["repository"] def get_username_from_keyring(self) -> Optional[str]: + if keyring is None: + logger.info("keyring module is not available") + return None try: system = cast(str, self.system) logger.info("Querying keyring for username") @@ -74,6 +85,9 @@ def get_username_from_keyring(self) -> Optional[str]: return None def get_password_from_keyring(self) -> Optional[str]: + if keyring is None: + logger.info("keyring module is not available") + return None try: system = cast(str, self.system) username = cast(str, self.username)