Repository: pyinstaller/pyinstaller-hooks-contrib (language: python)

Issue:
ModuleNotFoundError: No module named 'dbus_fast._private.marshaller'
**Describe the bug**
ModuleNotFoundError: No module named 'dbus_fast._private.marshaller'

* Which hook/library isn't working? 
dbus_fast(version: 2.11.1)

* Does the error get raised while building or when running?
Nothing to raise

**To Reproduce**

A minimal example file:
```
from dbus_fast.message import Message
```

PyInstaller command:
```
pyinstall test.py
```

Error:
```
  File "try_usb.py", line 25, in <module>
  File "PyInstaller/loader/pyimod02_importers.py", line 391, in exec_module
  File "dbus_fast/__init__.py", line 1, in <module>
  File "PyInstaller/loader/pyimod02_importers.py", line 391, in exec_module
  File "dbus_fast/introspection.py", line 5, in <module>
  File "PyInstaller/loader/pyimod02_importers.py", line 391, in exec_module
  File "dbus_fast/errors.py", line 57, in <module>
  File "src/dbus_fast/message.py", line 1, in init dbus_fast.message
ModuleNotFoundError: No module named 'dbus_fast._private.marshaller'
[11115] Failed to execute script 'try_usb' due to unhandled exception!
```

**Desktop (please complete the following information):**
 - OS: Manjaro
 - Python Version: python 3.11.5
 - Version of `pyinstaller-hooks-contrib`: 2023.10
 - Version of PyInstaller: 6.1.0




Discussion / hints:
Also observed with PyInstaller 6.6 on Ubuntu 22.04 LTS.

Fix patch:
diff --git a/news/600.new.rst b/news/600.new.rst
new file mode 100644
index 00000000..23648d33
--- /dev/null
+++ b/news/600.new.rst
@@ -0,0 +1,2 @@
+Add hook for ``dbus_fast`` in order to collect submodules that are imported
+from cythonized extensions.
diff --git a/src/_pyinstaller_hooks_contrib/hooks/stdhooks/hook-dbus_fast.py b/src/_pyinstaller_hooks_contrib/hooks/stdhooks/hook-dbus_fast.py
new file mode 100644
index 00000000..5289ecd0
--- /dev/null
+++ b/src/_pyinstaller_hooks_contrib/hooks/stdhooks/hook-dbus_fast.py
@@ -0,0 +1,16 @@
+# ------------------------------------------------------------------
+# Copyright (c) 2024 PyInstaller Development Team.
+#
+# This file is distributed under the terms of the GNU General Public
+# License (version 2.0 or later).
+#
+# The full license is available in LICENSE.GPL.txt, distributed with
+# this software.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+# ------------------------------------------------------------------
+
+from PyInstaller.utils.hooks import collect_submodules
+
+# Collect all submodules to handle imports made from cythonized extensions.
+hiddenimports = collect_submodules('dbus_fast')


Test patch:
diff --git a/requirements-test-libraries.txt b/requirements-test-libraries.txt
index 629c6673..ef2dedfe 100644
--- a/requirements-test-libraries.txt
+++ b/requirements-test-libraries.txt
@@ -184,6 +184,9 @@ schwifty==2024.5.3
 # eccodes package requires the eccodes shared library provided by the environment (linux distribution, homebrew, or Anaconda).
 eccodes==1.7.0; sys_platform == 'darwin' or sys_platform == 'linux'
 
+# dbus-fast has pre-built wheels only for Linux; and D-Bus is available only there, anyway.
+dbus-fast==2.21.2; sys_platform == 'linux'
+
 # PyEnchant only pre-builds macOS and Windows
 pyenchant==3.2.2; sys_platform == 'darwin' or sys_platform == 'win32'
 
diff --git a/src/_pyinstaller_hooks_contrib/tests/test_libraries.py b/src/_pyinstaller_hooks_contrib/tests/test_libraries.py
index d44eaa42..cec5419a 100644
--- a/src/_pyinstaller_hooks_contrib/tests/test_libraries.py
+++ b/src/_pyinstaller_hooks_contrib/tests/test_libraries.py
@@ -2057,3 +2057,43 @@ def test_eccodes_gribapi(pyi_builder):
 
         assert os.path.isfile(lib_filename), f"Shared library {lib_filename!s} not found!"
     """)
+
+
+@importorskip('dbus_fast')
+def test_dbus_fast(pyi_builder):
+    pyi_builder.test_source("""
+        import os
+        import sys
+        import asyncio
+        import json
+
+        from dbus_fast import Message, MessageType
+        from dbus_fast.aio import MessageBus
+
+
+        async def main():
+            # Connect to bus
+            try:
+                bus = await MessageBus().connect()
+            except Exception as e:
+                print(f"Could not connect to bus: {e}")
+                return
+
+            # List all available names
+            reply = await bus.call(
+                Message(
+                    destination="org.freedesktop.DBus",
+                    path="/org/freedesktop/DBus",
+                    interface="org.freedesktop.DBus",
+                    member="ListNames",
+                )
+            )
+
+            if reply.message_type == MessageType.ERROR:
+                raise Exception(reply.body[0])
+
+            print(json.dumps(reply.body[0], indent=2))
+
+
+        asyncio.run(main())
+    """)