Repository: jaraco/keyring

Issue:
Posible regresion in 25.4.0  breaking  keyrings-google-artifactregistry-auth
**Describe the bug** 
Posible regresion in  25.4.0 causing ValueError: Anonymous credential has no username with keyrings-google-artifactregistry-auth 

rolling back 25.3.0 works 


**To Reproduce**
Steps to reproduce the behavior:

1. install uv
2. uv tool install "keyring" --with keyrings-google-artifactregistry-auth
3. have dependecies managed by uv in toml
4. have python artifact registry on GCP
5. have toml set up correctly like
```toml
[tool.uv]
package = false
index-url = "https://oauth2accesstoken@europe-python.pkg.dev/<REDACTED>/simple/"
keyring-provider = "subprocess"
```

**Expected behavior**
authenticates and works

**Observed  behavior**
```
.811   File "/root/.local/bin/keyring", line 8, in <module>                                                                                                                                                                       
1.812     sys.exit(main())                                                                                                                                                                                                         
1.812              ^^^^^^                                                                                                                                                                                                          
1.813   File "/root/.local/share/uv/tools/keyring/lib/python3.12/site-packages/keyring/cli.py", line 219, in main
1.813     return cli.run(argv)
1.813            ^^^^^^^^^^^^^
1.813   File "/root/.local/share/uv/tools/keyring/lib/python3.12/site-packages/keyring/cli.py", line 120, in run
1.813     return method()
1.813            ^^^^^^^^
1.813   File "/root/.local/share/uv/tools/keyring/lib/python3.12/site-packages/keyring/cli.py", line 132, in do_get
1.814     getattr(self, f'_emit_{self.output_format}')(credential)
1.814   File "/root/.local/share/uv/tools/keyring/lib/python3.12/site-packages/keyring/cli.py", line 140, in _emit_plain
1.814     if credential.username:
1.814        ^^^^^^^^^^^^^^^^^^^
1.814   File "/root/.local/share/uv/tools/keyring/lib/python3.12/site-packages/keyring/credentials.py", line 39, in username
1.814     raise ValueError("Anonymous credential has no username")
1.814 ValueError: Anonymous credential has no username
```


**Environment**

- OS: latest docker  python:3.12-slim

```
$ uv tool list | grep keyring
keyring v25.4.0

$ uv run keyring --list-backends
keyring.backends.chainer.ChainerBackend (priority: -1)
keyrings.gauth.GooglePythonAuth (priority: 9)
keyring.backends.fail.Keyring (priority: 0)
```

**Additional context**
 keyrings-google-artifactregistry-auth works with pip/uv to authenticate user with Aplication default creds (passwordless) 
to allow private repositores and  private pipy proxy on GCP

Discussion:
@koterpillar: I created a broader #694 as it doesn't seem to have anything to do with Google.@jaraco: Let's track in #694.

Solution patch:
diff --git a/keyring/cli.py b/keyring/cli.py
index 8b3d712c..2c0ba4d3 100644
--- a/keyring/cli.py
+++ b/keyring/cli.py
@@ -132,14 +132,11 @@ def do_get(self):
         getattr(self, f'_emit_{self.output_format}')(credential)
 
     def _emit_json(self, credential: credentials.Credential):
-        print(
-            json.dumps(dict(username=credential.username, password=credential.password))
-        )
+        print(json.dumps(credential._vars()))
 
     def _emit_plain(self, credential: credentials.Credential):
-        if credential.username:
-            print(credential.username)
-        print(credential.password)
+        for val in credential._vars().values():
+            print(val)
 
     def _get_creds(self) -> credentials.Credential | None:
         return get_credential(self.service, self.username)
diff --git a/keyring/credentials.py b/keyring/credentials.py
index 4b28c6c7..6a2cecd8 100644
--- a/keyring/credentials.py
+++ b/keyring/credentials.py
@@ -13,6 +13,9 @@ def username(self) -> str: ...
     @abc.abstractproperty
     def password(self) -> str: ...
 
+    def _vars(self) -> dict[str, str]:
+        return dict(username=self.username, password=self.password)
+
 
 class SimpleCredential(Credential):
     """Simple credentials implementation"""
@@ -38,6 +41,9 @@ def __init__(self, password: str):
     def username(self) -> str:
         raise ValueError("Anonymous credential has no username")
 
+    def _vars(self) -> dict[str, str]:
+        return dict(password=self.password)
+
 
 class EnvironCredential(Credential):
     """
diff --git a/newsfragments/694.bugfix.rst b/newsfragments/694.bugfix.rst
new file mode 100644
index 00000000..be4c2559
--- /dev/null
+++ b/newsfragments/694.bugfix.rst
@@ -0,0 +1 @@
+Fixed ValueError for AnonymousCredentials in CLI.
\ No newline at end of file


Test patch:
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 435c55a3..87c3a2fc 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -6,6 +6,7 @@
 import pytest
 
 from keyring import cli
+from keyring import credentials
 
 flatten = itertools.chain.from_iterable
 
@@ -36,6 +37,12 @@ def mocked_set():
         yield set_password
 
 
+@pytest.fixture
+def mocked_get_credential():
+    with mock.patch('keyring.cli.get_credential') as get_credential:
+        yield get_credential
+
+
 def test_set_interactive(monkeypatch, mocked_set):
     tool = cli.CommandLineTool()
     tool.service = 'svc'
@@ -64,3 +71,27 @@ def test_set_pipe_newline(monkeypatch, mocked_set):
     monkeypatch.setattr(sys.stdin, 'read', lambda: 'foo123\n')
     tool.do_set()
     mocked_set.assert_called_once_with('svc', 'usr', 'foo123')
+
+
+@pytest.mark.parametrize('format', ['json', 'plain'])
+def test_get_anonymous(monkeypatch, mocked_get_credential, format, capsys):
+    mocked_get_credential.return_value = credentials.AnonymousCredential('s3cret')
+    tool = cli.CommandLineTool()
+    tool.service = 'svc'
+    tool.username = None
+    tool.get_mode = 'creds'
+    tool.output_format = format
+    tool.do_get()
+    assert 's3cret' in capsys.readouterr().out
+
+
+@pytest.mark.parametrize('format', ['json', 'plain'])
+def test_get(monkeypatch, mocked_get_credential, format, capsys):
+    mocked_get_credential.return_value = credentials.SimpleCredential('alice', 's3cret')
+    tool = cli.CommandLineTool()
+    tool.service = 'svc'
+    tool.username = 'alice'
+    tool.get_mode = 'creds'
+    tool.output_format = format
+    tool.do_get()
+    assert 's3cret' in capsys.readouterr().out