Repository: qtile/qtile

Issue:
initializer for ctype 'PangoLayout *' must be a cdata pointer, not NoneType
### The issue:

I am seeing this repeated many times in the log:

```bash
2022-09-30 15:55:42,265 ERROR libqtile loop.py:_handle_exception():L63 initializer for ctype 'PangoLayout *' must be a cdata pointer, not NoneType
NoneType: None
2022-09-30 15:55:42,265 ERROR libqtile loop.py:_handle_exception():L63 initializer for ctype 'PangoLayout *' must be a cdata pointer, not NoneType
NoneType: None
2022-09-30 15:55:42,265 ERROR libqtile loop.py:_handle_exception():L63 initializer for ctype 'PangoLayout *' must be a cdata pointer, not NoneType
NoneType: None
2022-09-30 15:55:42,265 ERROR libqtile loop.py:_handle_exception():L63 initializer for ctype 'PangoLayout *' must be a cdata pointer, not NoneType
NoneType: None
2022-09-30 15:55:42,265 ERROR libqtile loop.py:_handle_exception():L63 initializer for ctype 'PangoLayout *' must be a cdata pointer, not NoneType
NoneType: None
2022-09-30 15:55:42,265 ERROR libqtile loop.py:_handle_exception():L63 initializer for ctype 'PangoLayout *' must be a cdata pointer, not NoneType
NoneType: None
2022-09-30 15:55:42,265 ERROR libqtile loop.py:_handle_exception():L63 initializer for ctype 'PangoLayout *' must be a cdata pointer, not NoneType
NoneType: None
```

#2307, #2557 might be related? They at least mentioned the error, but neither led to a fix as far as I can tell. 

### Required:

- [X] I have searched past issues to see if this bug has already been reported.


Discussion:
I do get this message sometimes too. It's something I do want to look into.
Yeah I too get the same errors.

```
2022-10-01 14:09:50,268 ERROR libqtile base.py:on_done():L802 Failed to reschedule.
Traceback (most recent call last):
  File "/home/justine/.local/lib/python3.10/site-packages/libqtile/widget/base.py", line 794, in on_done
    self.update(result)
  File "/home/justine/.local/lib/python3.10/site-packages/libqtile/widget/base.py", line 697, in update
    old_width = self.layout.width
  File "/home/justine/.local/lib/python3.10/site-packages/libqtile/drawer.py", line 81, in width
    return self.layout.get_pixel_size()[0]
  File "/home/justine/.local/lib/python3.10/site-packages/libqtile/pangocffi.py", line 138, in get_pixel_size
    pango.pango_layout_get_pixel_size(self._pointer, width, height)
TypeError: initializer for ctype 'PangoLayout *' must be a cdata pointer, not NoneType
```


I'm getting this too, reproducible when I click play/pause on a Youtube video in Firefox.
> I'm getting this too, reproducible when I click play/pause on a Youtube video in Firefox.

What widgets do you have in your bar? This is related to text display.
Here's my [widgets.py](https://codeberg.org/JustineSmithies/qtile-wayland-dotfiles/src/branch/master/.config/qtile/[widgets.py](https://codeberg.org/JustineSmithies/qtile-wayland-dotfiles/src/branch/master/.config/qtile/widgets.py)) if that helps ?

Thanks.

It was more a question for @daveoconnor 

We really need to be able to recreate this reliably so I need more info. The commend about youtube on Firefox is helpful but, as I watch Youtube on Firefox and don't see this, I need to know what widgets they have in their bar.

I can trigger it reliably with the default config by making the reload config binding run it twice in quick succession:
```diff
diff --git a/libqtile/resources/default_config.py b/libqtile/resources/default_config.py
index 5408b07b..1dfd7ee7 100644
--- a/libqtile/resources/default_config.py
+++ b/libqtile/resources/default_config.py
@@ -68,7 +68,7 @@ keys = [
     # Toggle between different layouts as defined below
     Key([mod], "Tab", lazy.next_layout(), desc="Toggle between layouts"),
     Key([mod], "w", lazy.window.kill(), desc="Kill focused window"),
-    Key([mod, "control"], "r", lazy.reload_config(), desc="Reload the config"),
+    Key([mod, "control"], "r", lazy.reload_config(), lazy.reload_config(), desc="Reload the config"),
     Key([mod, "control"], "q", lazy.shutdown(), desc="Shutdown Qtile"),
     Key([mod], "r", lazy.spawncmd(), desc="Spawn a command using a prompt widget"),
 ]

```
Thanks. Let me investigate.
It looks like this is to do with timers triggering a draw on widgets that have been finalised. That shouldn't happen as we delete timers when widgets are finalised so something's going wrong.

Resolving patch:
diff --git a/libqtile/widget/base.py b/libqtile/widget/base.py
--- a/libqtile/widget/base.py
+++ b/libqtile/widget/base.py
@@ -170,7 +170,7 @@ def __init__(self, length, **config):
             raise confreader.ConfigError("Widget width must be an int")
 
         self.configured = False
-        self._futures: list[asyncio.TimerHandle] = []
+        self._futures: list[asyncio.Handle] = []
         self._mirrors: set[_Widget] = set()
 
     @property
@@ -224,9 +224,15 @@ def _configure(self, qtile, bar):
         self.qtile = qtile
         self.bar = bar
         self.drawer = bar.window.create_drawer(self.bar.width, self.bar.height)
+
+        # Timers are added to futures list so they can be cancelled if the `finalize` method is
+        # called before the timers have fired.
         if not self.configured:
-            self.qtile.call_soon(self.timer_setup)
-            self.qtile.call_soon(create_task, self._config_async())
+            timer = self.qtile.call_soon(self.timer_setup)
+            async_timer = self.qtile.call_soon(asyncio.create_task, self._config_async())
+
+            # Add these to our list of futures so they can be cancelled.
+            self._futures.extend([timer, async_timer])
 
     async def _config_async(self):
         """
@@ -342,10 +348,27 @@ def call_process(self, command, **kwargs):
 
     def _remove_dead_timers(self):
         """Remove completed and cancelled timers from the list."""
+
+        def is_ready(timer):
+            return timer in self.qtile._eventloop._ready
+
         self._futures = [
             timer
             for timer in self._futures
-            if not (timer.cancelled() or timer.when() < self.qtile._eventloop.time())
+            # Filter out certain handles...
+            if not (
+                timer.cancelled()
+                # Once a scheduled timer is ready to be run its _scheduled flag is set to False
+                # and it's added to the loop's `_ready` queue
+                or (
+                    isinstance(timer, asyncio.TimerHandle)
+                    and not timer._scheduled
+                    and not is_ready(timer)
+                )
+                # Callbacks scheduled via `call_soon` are put into the loop's `_ready` queue
+                # and are removed once they've been executed
+                or (isinstance(timer, asyncio.Handle) and not is_ready(timer))
+            )
         ]
 
     def _wrapper(self, method, *method_args):


Test patch:
diff --git a/test/widgets/test_base.py b/test/widgets/test_base.py
--- a/test/widgets/test_base.py
+++ b/test/widgets/test_base.py
@@ -46,7 +46,7 @@ def cancel_timer2(self):
 
     @expose_command()
     def get_active_timers(self):
-        active = [x for x in self._futures if x._scheduled]
+        active = [x for x in self._futures if getattr(x, "_scheduled", False)]
         return len(active)
 
 
diff --git a/test/widgets/test_chord.py b/test/widgets/test_chord.py
--- a/test/widgets/test_chord.py
+++ b/test/widgets/test_chord.py
@@ -111,7 +111,10 @@ def test_chord_widget(fake_window, fake_qtile):
     assert chord.background == BASE_BACKGROUND
     assert chord.foreground == BASE_FOREGROUND
 
-    # Finalize the widget to prevent segfault
+    # Finalize the widget to prevent segfault (the drawer needs to be finalised)
+    # We clear the _futures attribute as there are no real timers in it and calls
+    # to `cancel()` them will fail.
+    chord._futures = []
     chord.finalize()