Repository: qtile/qtile

Issue:
`ImapWidget` may call `keyring.get_password()` with `username=None`, violating API and potentially crashing it
### Issue description

The problematic code is:

https://github.com/qtile/qtile/blob/9ccaf6f1c01a9ffbd7beacdd8f405884bd81e1c0/libqtile/widget/imapwidget.py#L78

At this point, `self.user` may be `None`. However, according to the API definition at:

https://github.com/jaraco/keyring/blob/0cebfebbf516a47e4e45911ba6b4d4dd2699845c/keyring/core.py#L54

`keyring.get_password()` expects two `str` argument, i.e. `None` is not acceptable. If `keyrings-alt` backend is installed, then it explicitly crashes on `None` username:

```pytb
libqtile/widget/imapwidget.py:78: in __init__
    password = keyring.get_password("imapwidget", self.user)
.tox/py310-x11/lib/python3.10/site-packages/keyring/core.py:56: in get_password
    return get_keyring().get_password(service_name, username)
.tox/py310-x11/lib/python3.10/site-packages/keyrings/alt/file_base.py:92: in get_password
    assoc = self._generate_assoc(service, username)
.tox/py310-x11/lib/python3.10/site-packages/keyrings/alt/file_base.py:133: in _generate_assoc
    return (escape_for_ini(service) + r'\0' + escape_for_ini(username)).encode()
.tox/py310-x11/lib/python3.10/site-packages/keyrings/alt/escape.py:29: in escape
    return "".join(_escape_char(c) for c in value.encode('utf-8'))
E   AttributeError: 'NoneType' object has no attribute 'encode'
```

To reproduce:

```
tox -e py310-x11  # you can cancel the initial test run, after dependencies are installed
. .tox/py310-x11/bin/activate
pip install imapclient keyring keyrings-alt
pytest --backend=x11
```

### Version

0.23.1.dev83+g9ccaf6f1

### Backend

X11 (default)

### Config

_No response_

### Logs

_No response_

### Required

- [X] I have searched past issues to see if this bug has already been reported, and it hasn't been.
- [X] I understand that people give their precious time for free, and thus I've done my very best to make this problem as easy as possible to investigate.


Discussion:
Are you willing to submit a PR with a fix?
Yeah, I'm trying to make one right now.

Resolving patch:
diff --git a/libqtile/widget/imapwidget.py b/libqtile/widget/imapwidget.py
--- a/libqtile/widget/imapwidget.py
+++ b/libqtile/widget/imapwidget.py
@@ -24,6 +24,7 @@
 
 import keyring
 
+from libqtile.confreader import ConfigError
 from libqtile.log_utils import logger
 from libqtile.widget import base
 
@@ -75,6 +76,8 @@ class ImapWidget(base.ThreadPoolText):
     def __init__(self, **config):
         base.ThreadPoolText.__init__(self, "", **config)
         self.add_defaults(ImapWidget.defaults)
+        if self.user is None:
+            raise ConfigError("You must set the 'user' parameter for the IMAP widget.")
         password = keyring.get_password("imapwidget", self.user)
         if password is not None:
             self.password = password


Test patch:
diff --git a/test/widgets/test_widget_init_configure.py b/test/widgets/test_widget_init_configure.py
--- a/test/widgets/test_widget_init_configure.py
+++ b/test/widgets/test_widget_init_configure.py
@@ -58,6 +58,7 @@
 
 # To skip a test entirely, list the widget class here
 no_test = [widgets.Mirror, widgets.PulseVolume]  # Mirror requires a reflection object
+no_test += [widgets.ImapWidget]  # Requires a configured username
 
 # To test a widget only under one backend, list the widget class here
 exclusive_backend = {