Repository: jaraco/keyring

Issue:
"Anonymous credential has no username" on keyring get
**Describe the bug**
Getting existing credentials using `keyring get` fails with an exception stacktrace showing "Anonymous credential has no username".

**To Reproduce**
Steps to reproduce the behavior:

```shell
$ keyring set aaaa bbbb
Password for 'bbbb' in 'aaaa': <enter password>
$ keyring get aaaa bbbb
Traceback (most recent call last):
  File "<snip>/keyring", line 8, in <module>
    sys.exit(main())
             ^^^^^^
  File "<snip>/venvs/keyring/lib/python3.12/site-packages/keyring/cli.py", line 219, in main
    return cli.run(argv)
           ^^^^^^^^^^^^^
  File "<snip>/venvs/keyring/lib/python3.12/site-packages/keyring/cli.py", line 120, in run
    return method()
           ^^^^^^^^
  File "<snip>/venvs/keyring/lib/python3.12/site-packages/keyring/cli.py", line 132, in do_get
    getattr(self, f'_emit_{self.output_format}')(credential)
  File "<snip>/venvs/keyring/lib/python3.12/site-packages/keyring/cli.py", line 140, in _emit_plain
    if credential.username:
       ^^^^^^^^^^^^^^^^^^^
  File "<snip>/venvs/keyring/lib/python3.12/site-packages/keyring/credentials.py", line 39, in username
    raise ValueError("Anonymous credential has no username")
ValueError: Anonymous credential has no username
```

**Expected behavior**

`keyring get` should return the password set by `keyring set`.

**Environment**

- OS: macOS 14.6.1 (23G93)

```shell
$ pipx list | grep keyring <note: pipx>
package keyring 25.4.0, installed using Python 3.12.4
$ keyring --list-backends
keyring.backends.macOS.Keyring (priority: 5)
keyring.backends.fail.Keyring (priority: 0)
keyring.backends.chainer.ChainerBackend (priority: -1)
```

**Additional context**

Works fine with 25.3.0, even with a password set by 25.4.0.


Discussion:

Patch:
diff --git a/keyring/cli.py b/keyring/cli.py
index 8b3d712..2c0ba4d 100644
--- a/keyring/cli.py
+++ b/keyring/cli.py
@@ -132,14 +132,11 @@ class CommandLineTool:
         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 4b28c6c..6a2cecd 100644
--- a/keyring/credentials.py
+++ b/keyring/credentials.py
@@ -13,6 +13,9 @@ class Credential(metaclass=abc.ABCMeta):
     @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 @@ class AnonymousCredential(SimpleCredential):
     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 0000000..be4c255
--- /dev/null
+++ b/newsfragments/694.bugfix.rst
@@ -0,0 +1,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 435c55a..87c3a2f 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -6,6 +6,7 @@ from unittest import mock
 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