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

Issue:
MONAI package
**Which library is the hook for?**

[MONAI](https://github.com/Project-MONAI/MONAI)

**Have you gotten the library to work with pyinstaller?**

I have gotten it to install, but there are several warnings and I did not check if the functionality works.

**Additional context**

Based on [this issue](https://github.com/Project-MONAI/MONAI/discussions/4197#discussioncomment-6632871) in the MONAI repo, I created the following hook:

```python
from pathlib import Path

import monai
from PyInstaller.utils.hooks import collect_data_files

datas = [(str(Path(monai.__file__).parent), "monai"), *collect_data_files("monai")]
```

This allows my program to build, but when I run it there are several warnings:

```
WARNING: Internal configuration path is invalid: /home/mvine/project/dist/test_library/_internal/itk
WARNING: Invalid: swig_lib: location of the swig-generated shared libraries
WARNING: Internal configuration path is invalid: /home/mvine/project/dist/test_library/_internal/itk
WARNING: Invalid: swig_py: location of the xxxPython.py swig-generated python interfaces
WARNING: Internal configuration path is invalid: /home/mvine/project/dist/test_library/_internal/itk/Configuration
WARNING: Invalid: config_py: location of xxxConfig.py CMake-generated library descriptions
```

I also don't think the datas list is optimal as it is collecting many many files and the root directory?


Discussion / hints:
> ```
> datas = [(str(Path(monai.__file__).parent), "monai"), *collect_data_files("monai")]
> ```

Why bother calling `collect_data_files` on the package if you already brute-force collect complete package directory?
Hmm, looks like all this needs is source .py files collected, and this can be achieved simply by setting module collection mode.
How is that done?
With [module_collection_mode](https://pyinstaller.org/en/stable/hooks.html?highlight=module_collection_mode#hook-global-variables)
You can take `fairscale` as an example:
https://github.com/pyinstaller/pyinstaller-hooks-contrib/blob/master/_pyinstaller_hooks_contrib/stdhooks/hook-fairscale.py

https://github.com/pyinstaller/pyinstaller-hooks-contrib/blob/28151eae3bc6f026c26469e0960e4aad5d689b17/tests/test_deep_learning.py#L355-L361

Note that while you can add a basic import test like I did for `fairscale`, we generally do not add torch-based packages to requirements file.
But do also check with your application, in case we need any hidden imports added via `collect_submodules` that are not caught by the basic import test.
Sure thing, I will open a pull request in a while
That throws the same warning about the config path and swig library...
Those look like they are about `itk`, not `monai`.
> Those look like they are about `itk`, not `monai`.

A basic `import itk` test shows that `itk/Configuration` directory is expected to exist. In the case of the test, this requires both `hiddenimports = collect_submodules('itk.Configuration')` and module collected mode for `itk.Configuration` being set to `pyz+py` (so that .py source files result in creation of the directory).
> module collected mode for `itk.Configuration`

By this do you mean another hook (`hook-itk.Configuration.py`) with the module collection mode specified? (Sorry for so many inquires, I'm learning)
> > module collected mode for `itk.Configuration`
> 
> By this do you mean another hook (`hook-itk.Configuration.py`) with the module collection mode specified? (Sorry for so many inquires, I'm learning)

No, it will have to be `hook-itk.py` (because `itk.Configuration` might not be collected on its own, and thus the `hook-itk.Configuration.py` would not be triggered at all):

```python
# hook-itk.py
from PyInstaller.utils.hooks import collect_submodules

hiddenimports = collect_submodules('itk.Configuration')

# itk.Configuration modules being also collected as source .py files ensures that itk/Configuration directory is created.
module_collection_mode = {
    'itk.Configuration': 'pyz+py',
}
```

> (Sorry for so many inquires, I'm learning)

No worries, there's a lot of black magic involved, especially for more complex stuff.

Fix patch:
diff --git a/_pyinstaller_hooks_contrib/stdhooks/hook-itk.py b/_pyinstaller_hooks_contrib/stdhooks/hook-itk.py
new file mode 100644
index 00000000..54bf624c
--- /dev/null
+++ b/_pyinstaller_hooks_contrib/stdhooks/hook-itk.py
@@ -0,0 +1,21 @@
+# ------------------------------------------------------------------
+# 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, distributed with
+# this software.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+# ------------------------------------------------------------------
+
+from PyInstaller.utils.hooks import collect_submodules
+
+hiddenimports = collect_submodules("itk.Configuration")
+
+# `itk` requires `itk/Configuration` directory to exist on filesystem; collect source .py files from `itk.Configuration`
+# as a work-around that ensures the existence of this directory.
+module_collection_mode = {
+    "itk.Configuration": "pyz+py",
+}
diff --git a/_pyinstaller_hooks_contrib/stdhooks/hook-monai.py b/_pyinstaller_hooks_contrib/stdhooks/hook-monai.py
new file mode 100644
index 00000000..82676dbc
--- /dev/null
+++ b/_pyinstaller_hooks_contrib/stdhooks/hook-monai.py
@@ -0,0 +1,14 @@
+# ------------------------------------------------------------------
+# 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, distributed with
+# this software.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+# ------------------------------------------------------------------
+
+# Collect source .py files for JIT/torchscript. Requires PyInstaller >= 5.3, no-op in older versions.
+module_collection_mode = "pyz+py"
diff --git a/news/778.new.1.rst b/news/778.new.1.rst
new file mode 100644
index 00000000..bb3fcc76
--- /dev/null
+++ b/news/778.new.1.rst
@@ -0,0 +1,2 @@
+Add hooks for ``itk`` to work around the package's requirements about
+the ``itk/Configuration`` directory.
diff --git a/news/778.new.rst b/news/778.new.rst
new file mode 100644
index 00000000..1538c465
--- /dev/null
+++ b/news/778.new.rst
@@ -0,0 +1,1 @@
+Add hook for ``monai`` to collect its source .py files for TorchScript/JIT.


Test patch:
diff --git a/tests/test_deep_learning.py b/tests/test_deep_learning.py
index e65c30a5..cf93124c 100644
--- a/tests/test_deep_learning.py
+++ b/tests/test_deep_learning.py
@@ -359,3 +359,11 @@ def test_fairscale(pyi_builder):
     pyi_builder.test_source("""
         import fairscale
     """)
+
+
+# Basic import test for monai, which shows that we need to collect its source .py files for TorchScript/JIT.
+@importorskip('monai')
+def test_monai(pyi_builder):
+    pyi_builder.test_source("""
+        import monai
+    """)
diff --git a/tests/test_libraries.py b/tests/test_libraries.py
index 21d5de66..587b104c 100644
--- a/tests/test_libraries.py
+++ b/tests/test_libraries.py
@@ -2143,3 +2143,10 @@ def test_pyexcel_ods(pyi_builder):
     pyi_builder.test_source("""
         import pyexcel_ods
     """)
+
+
+@importorskip('itk')
+def test_itk(pyi_builder):
+    pyi_builder.test_source("""
+        import itk
+    """)