Repository: pyinstaller/pyinstaller

Issue:
Keyring hook does not work
Environment: 
- MacOS 10.14.6
- Python 3.7.4 (from homebrew)
- PyInstaller==4.0.dev0+9dd34bdfba
- keyring==20.0.0

Configure the virtualenv with keyring and pyinstaller
```bash
python3 -m venv venv
source ./venv/bin/activate
pip install keyring 
pip install https://github.com/pyinstaller/pyinstaller/archive/develop.zip
```

Have a simple file called `test.py`:
```python
import keyring
print(keyring.get_password('test','test'))
```
This prints "None" when executed.

Package it and run:
```bash
pyinstaller --clean --log-level DEBUG -d all --noupx -y test.py
```

Build log: https://pastebin.com/raw/VeEtsZge
Content of build and dist: https://www.dropbox.com/s/79s7fe8dpkij717/pyinstbug.tgz?dl=1

Result:
```
$> ./dist/test/test

Traceback (most recent call last):
  File "test.py", line 1, in <module>
  File "<frozen importlib._bootstrap>", line 983, in _find_and_load
  File "<frozen importlib._bootstrap>", line 967, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 677, in _load_unlocked
  File "/Users/toyg/Dev/tmp/venv/lib/python3.7/site-packages/PyInstaller/loader/pyimod03_importers.py", line 627, in exec_module
    exec(bytecode, module.__dict__)
  File "keyring/__init__.py", line 1, in <module>
  File "<frozen importlib._bootstrap>", line 983, in _find_and_load
  File "<frozen importlib._bootstrap>", line 967, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 677, in _load_unlocked
  File "/Users/toyg/Dev/tmp/venv/lib/python3.7/site-packages/PyInstaller/loader/pyimod03_importers.py", line 627, in exec_module
    exec(bytecode, module.__dict__)
  File "keyring/core.py", line 192, in <module>
  File "keyring/core.py", line 96, in init_backend
  File "keyring/util/__init__.py", line 22, in wrapper
  File "keyring/backend.py", line 216, in get_all_keyring
  File "keyring/backend.py", line 199, in _load_plugins
KeyError: 'keyring.backends'
[55646] Failed to execute script test
```

From the various logs it looks like keyring is somewhat detected, but then it's not actually fully imported at runtime. I have tried explicitly setting all keyring submodules as hiddenimports, it does not make any difference.



Discussion:
For reference for anyone who like me landed here out of the blue, @msk849 has submitted a [PR for this issue](https://github.com/jaraco/keyring/pull/416), and until it makes its way to master to keyring release to pyinstaller to pyinstaller release to pypi, ...... there's a horrible workaround to get it to work _right now_ that in theory you should not do, which is altering the library yourself with the contents of their PR directly in your own `site-packages` python package cache.

Open `site-packages/keyring-x.x.x-pyx.x.egg/keyring/backend.py` and replace
```
    entry_points = metadata.entry_points()['keyring.backends']
```
with
```
    try: entry_points = metadata.entry_points()['keyring.backends']
    except Exception:
        entry_points = (
            metadata.EntryPoint(name='KWallet',value='keyring.backends.kwallet',group='keyring.backends'),
            metadata.EntryPoint(name='SecretService',value='keyring.backends.SecretService',group='keyring.backends'),
            metadata.EntryPoint(name='Windows',value='keyring.backends.Windows',group='keyring.backends'),
            metadata.EntryPoint(name='chainer',value='keyring.backends.chainer',group='keyring.backends'),
            metadata.EntryPoint(name='macOS',value='keyring.backends.OS_X',group='keyring.backends'))
```
which should be around line 199.

Clear pyinstaler build and dist cache, rerun pyinstaller, and the error should be gone.


@anasofiapaixao thanks for the update, I'm facing the same issue.
Is there anything that can be done in a PyInstaller hook to overcome this issue?
My issue is that I'm currently using Python2 and even if this fix will find its way to master it will only be for Python3 (18.0.1 is the last release that supports Python2).

My current workaround without changing the `keyring` code is to add this code after `import keyring`:
```
if hasattr(sys, "frozen"):
    if sys.platform.startswith("win"):
        import keyring.backends.Windows
        keyring.set_keyring(keyring.backends.Windows.WinVaultKeyring())
    elif sys.platform.startswith("darwin"):
        import keyring.backends.OS_X
        keyring.set_keyring(keyring.backends.OS_X.Keyring())
```
Doesn't look like that Keyring PR is going to be merged, maintainers have mentioned that PyInstaller should supply the metadata so that other packages should be able to follow [these instructions](https://docs.python.org/3/library/importlib.metadata.html#extending-the-search-algorithm) to discover the metadata.
Newer Keyrings also fail to work in apps bundled with py2app. Can't find any workaround despite modifying the keyring lib. Monkey patching seems not to be possible.  So I downgrade to `keyring==19.2.0` again. Bad.
BTW, stumbling upon this error myself while I'm also learning the Rust programming language (A systems language like C\C++), I've made an attempt to port Rust's own most popular keyring library to Python and it seems to be working fine with PyInstaller.
It's cross-platform as well but I've mainly tested it on windows.

`pip install rskeyring` 

Resolving patch:
diff --git a/PyInstaller/hooks/hook-keyring.backends.py b/PyInstaller/hooks/hook-keyring.py
similarity index 63%
rename from PyInstaller/hooks/hook-keyring.backends.py
rename to PyInstaller/hooks/hook-keyring.py
--- a/PyInstaller/hooks/hook-keyring.backends.py
+++ b/PyInstaller/hooks/hook-keyring.py
@@ -9,8 +9,12 @@
 # SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
 #-----------------------------------------------------------------------------
 
-# Tested with keyring 3.7 on MacOS.
-
-from PyInstaller.utils.hooks import collect_submodules
+from PyInstaller.utils.hooks import collect_submodules, copy_metadata
 
+# Collect backends
 hiddenimports = collect_submodules('keyring.backends')
+
+# Keyring performs backend plugin discovery using setuptools entry points,
+# which are listed in the metadata. Therefore, we need to copy the
+# metadata, otherwise no backends will be found at run-time.
+datas = copy_metadata('keyring')


Test patch:
diff --git a/tests/functional/test_libraries.py b/tests/functional/test_libraries.py
--- a/tests/functional/test_libraries.py
+++ b/tests/functional/test_libraries.py
@@ -361,8 +361,15 @@ def test_idlelib(pyi_builder):
 
 
 @importorskip('keyring')
+@skipif(is_linux, reason="SecretStorage backend on linux requires active "
+                         "D-BUS session and initialized keyring, and may "
+                         "need to unlock the keyring via UI prompt.")
 def test_keyring(pyi_builder):
-    pyi_builder.test_source("import keyring")
+    pyi_builder.test_source(
+        """
+        import keyring
+        keyring.get_password("test", "test")
+        """)
 
 
 @importorskip('numpy')