Skip to content

Commit 62f3c67

Browse files
fedexistFederico D'Ambrosiobhrutledge
authored
Log keyring tracebacks (#890)
* Use logger.exception instead of logger.warning to handle keyring failures Add related test * Revert logging back to warning Update tests * Add changelog entry * Parameterize test * Use re.search for caplog * Group similar tests * Rework missing HOME setup * Refactor fixtures * Normalize prompt tests * Fix incorrect non-interactive test * Reload keyring to reproduce test failure * Patch pwd instead of os * Make assertions more specific * Move monkeypatch to keyring * Test attributes instead of methods * Use more specific log messages * Normalize keyring monkeypatch Co-authored-by: Federico D'Ambrosio <[email protected]> Co-authored-by: Brian Rutledge <[email protected]>
1 parent d30df70 commit 62f3c67

File tree

3 files changed

+81
-35
lines changed

3 files changed

+81
-35
lines changed

changelog/890.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Improve logging when keyring fails.

tests/test_auth.py

Lines changed: 78 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import getpass
22
import logging
3+
import re
34

45
import pytest
56

@@ -13,23 +14,23 @@ def config() -> utils.RepositoryConfig:
1314
return dict(repository="system")
1415

1516

16-
def test_get_password_keyring_overrides_prompt(monkeypatch, config):
17+
def test_get_username_keyring_defers_to_prompt(monkeypatch, entered_username, config):
1718
class MockKeyring:
1819
@staticmethod
19-
def get_password(system, user):
20-
return f"{user}@{system} sekure pa55word"
20+
def get_credential(system, user):
21+
return None
2122

2223
monkeypatch.setattr(auth, "keyring", MockKeyring)
2324

24-
pw = auth.Resolver(config, auth.CredentialInput("user")).password
25-
assert pw == "user@system sekure pa55word"
25+
username = auth.Resolver(config, auth.CredentialInput()).username
26+
assert username == "entered user"
2627

2728

2829
def test_get_password_keyring_defers_to_prompt(monkeypatch, entered_password, config):
2930
class MockKeyring:
3031
@staticmethod
3132
def get_password(system, user):
32-
return
33+
return None
3334

3435
monkeypatch.setattr(auth, "keyring", MockKeyring)
3536

@@ -51,7 +52,7 @@ def test_empty_password_bypasses_prompt(monkeypatch, entered_password, config):
5152

5253
def test_no_username_non_interactive_aborts(config):
5354
with pytest.raises(exceptions.NonInteractive):
54-
auth.Private(config, auth.CredentialInput("user")).password
55+
auth.Private(config, auth.CredentialInput()).username
5556

5657

5758
def test_no_password_non_interactive_aborts(config):
@@ -124,55 +125,99 @@ def test_get_password_keyring_missing_non_interactive_aborts(
124125
auth.Private(config, auth.CredentialInput("user")).password
125126

126127

127-
@pytest.fixture
128-
def keyring_no_backends(monkeypatch):
129-
"""Simulate missing keyring backend raising RuntimeError on get_password."""
130-
128+
def test_get_username_keyring_runtime_error_logged(
129+
entered_username, monkeypatch, config, caplog
130+
):
131131
class FailKeyring:
132+
"""Simulate missing keyring backend raising RuntimeError on get_credential."""
133+
132134
@staticmethod
133-
def get_password(system, username):
135+
def get_credential(system, username):
134136
raise RuntimeError("fail!")
135137

136-
monkeypatch.setattr(auth, "keyring", FailKeyring())
138+
monkeypatch.setattr(auth, "keyring", FailKeyring)
137139

140+
assert auth.Resolver(config, auth.CredentialInput()).username == "entered user"
138141

139-
@pytest.fixture
140-
def keyring_no_backends_get_credential(monkeypatch):
141-
"""Simulate missing keyring backend raising RuntimeError on get_credential."""
142+
assert re.search(
143+
r"Error getting username from keyring.+Traceback.+RuntimeError: fail!",
144+
caplog.text,
145+
re.DOTALL,
146+
)
142147

148+
149+
def test_get_password_keyring_runtime_error_logged(
150+
entered_username, entered_password, monkeypatch, config, caplog
151+
):
143152
class FailKeyring:
153+
"""Simulate missing keyring backend raising RuntimeError on get_password."""
154+
144155
@staticmethod
145-
def get_credential(system, username):
156+
def get_password(system, username):
146157
raise RuntimeError("fail!")
147158

148-
monkeypatch.setattr(auth, "keyring", FailKeyring())
159+
monkeypatch.setattr(auth, "keyring", FailKeyring)
149160

161+
assert auth.Resolver(config, auth.CredentialInput()).password == "entered pw"
150162

151-
def test_get_username_runtime_error_suppressed(
152-
entered_username, keyring_no_backends_get_credential, caplog, config
153-
):
154-
assert auth.Resolver(config, auth.CredentialInput()).username == "entered user"
155-
assert caplog.messages == ["fail!"]
163+
assert re.search(
164+
r"Error getting password from keyring.+Traceback.+RuntimeError: fail!",
165+
caplog.text,
166+
re.DOTALL,
167+
)
156168

157169

158-
def test_get_password_runtime_error_suppressed(
159-
entered_password, keyring_no_backends, caplog, config
160-
):
161-
assert auth.Resolver(config, auth.CredentialInput("user")).password == "entered pw"
162-
assert caplog.messages == ["fail!"]
163-
170+
def _raise_home_key_error():
171+
"""Simulate environment from https://github.com/pypa/twine/issues/889."""
172+
try:
173+
raise KeyError("HOME")
174+
except KeyError:
175+
raise KeyError("uid not found: 999")
164176

165-
def test_get_username_return_none(entered_username, monkeypatch, config):
166-
"""Prompt for username when it's not in keyring."""
167177

178+
def test_get_username_keyring_key_error_logged(
179+
entered_username, monkeypatch, config, caplog
180+
):
168181
class FailKeyring:
169182
@staticmethod
170183
def get_credential(system, username):
171-
return None
184+
_raise_home_key_error()
185+
186+
monkeypatch.setattr(auth, "keyring", FailKeyring)
172187

173-
monkeypatch.setattr(auth, "keyring", FailKeyring())
174188
assert auth.Resolver(config, auth.CredentialInput()).username == "entered user"
175189

190+
assert re.search(
191+
r"Error getting username from keyring"
192+
r".+Traceback"
193+
r".+KeyError: 'HOME'"
194+
r".+KeyError: 'uid not found: 999'",
195+
caplog.text,
196+
re.DOTALL,
197+
)
198+
199+
200+
def test_get_password_keyring_key_error_logged(
201+
entered_username, entered_password, monkeypatch, config, caplog
202+
):
203+
class FailKeyring:
204+
@staticmethod
205+
def get_password(system, username):
206+
_raise_home_key_error()
207+
208+
monkeypatch.setattr(auth, "keyring", FailKeyring)
209+
210+
assert auth.Resolver(config, auth.CredentialInput()).password == "entered pw"
211+
212+
assert re.search(
213+
r"Error getting password from keyring"
214+
r".+Traceback"
215+
r".+KeyError: 'HOME'"
216+
r".+KeyError: 'uid not found: 999'",
217+
caplog.text,
218+
re.DOTALL,
219+
)
220+
176221

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

twine/auth.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def get_username_from_keyring(self) -> Optional[str]:
6363
# To support keyring prior to 15.2
6464
pass
6565
except Exception as exc:
66-
logger.warning(str(exc))
66+
logger.warning("Error getting username from keyring", exc_info=exc)
6767
return None
6868

6969
def get_password_from_keyring(self) -> Optional[str]:
@@ -73,7 +73,7 @@ def get_password_from_keyring(self) -> Optional[str]:
7373
logger.info("Querying keyring for password")
7474
return cast(str, keyring.get_password(system, username))
7575
except Exception as exc:
76-
logger.warning(str(exc))
76+
logger.warning("Error getting password from keyring", exc_info=exc)
7777
return None
7878

7979
def username_from_keyring_or_prompt(self) -> str:

0 commit comments

Comments
 (0)