Repository: indico/flask-multipass

Issue:
Type error on init_app
```
File ".../site-packages/flask_multipass/util.py", line 146, in resolve_provider_type
entry_points = importlib_entry_points(group=base._entry_point, name=type_)

TypeError: entry_points() for an unexpected keyword argument 'group'
```

I see this error and version 0.5 dies while loading.
It's trying to call `importlib.metadata.entrypoints` but that doesn't have a group argument.


Comments:

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 4a63425..0817943 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,6 +1,11 @@
 Changelog
 =========
 
+Version 0.5.1
+-------------
+
+- Fix compatibility with Python 3.8 and 3.9
+
 Version 0.5
 -----------
 
diff --git a/flask_multipass/__init__.py b/flask_multipass/__init__.py
index 97505a8..ea6d783 100644
--- a/flask_multipass/__init__.py
+++ b/flask_multipass/__init__.py
@@ -13,7 +13,7 @@ from .group import Group
 from .identity import IdentityProvider
 
 
-__version__ = '0.5'
+__version__ = '0.5.1'
 __all__ = ('Multipass', 'AuthProvider', 'IdentityProvider', 'AuthInfo', 'IdentityInfo', 'Group', 'MultipassException',
            'AuthenticationFailed', 'IdentityRetrievalFailed', 'GroupRetrievalFailed', 'NoSuchUser',
            'InvalidCredentials')
diff --git a/flask_multipass/util.py b/flask_multipass/util.py
index f328970..4131f2e 100644
--- a/flask_multipass/util.py
+++ b/flask_multipass/util.py
@@ -4,6 +4,7 @@
 # Flask-Multipass is free software; you can redistribute it
 # and/or modify it under the terms of the Revised BSD License.
 
+import sys
 from functools import wraps
 from importlib.metadata import entry_points as importlib_entry_points
 from inspect import getmro, isclass
@@ -143,14 +144,17 @@ def resolve_provider_type(base, type_, registry=None):
     if registry is not None and type_ in registry:
         cls = registry[type_]
     else:
-        entry_points = importlib_entry_points(group=base._entry_point, name=type_)
+        if sys.version_info < (3, 10):
+            entry_points = {ep for ep in importlib_entry_points().get(base._entry_point, []) if ep.name == type_}
+        else:
+            entry_points = importlib_entry_points(group=base._entry_point, name=type_)
         if not entry_points:
             raise ValueError('Unknown type: ' + type_)
         elif len(entry_points) != 1:
             # TODO: remove the getattr check after dropping python 3.8
             defs = ', '.join(getattr(ep, 'module', ep.value) for ep in entry_points)
             raise RuntimeError(f'Type {type_} is not unique. Defined in {defs}')
-        entry_point = entry_points[0]
+        entry_point = list(entry_points)[0]
         cls = entry_point.load()
     if not issubclass(cls, base):
         raise TypeError(f'Found a class {cls} which is not a subclass of {base}')


Test patch:
diff --git a/tests/test_core.py b/tests/test_core.py
index 0adada1..ef1e60b 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -218,3 +218,12 @@ def test_handle_auth_error_with_redirect(mocker):
         multipass.handle_auth_error(AuthenticationFailed(), redirect_to_login=True)
         assert flash.called
         redirect.assert_called_with(app.config['MULTIPASS_LOGIN_URLS'][0])
+
+
+def test_load_providers_from_entrypoints():
+    app = Flask('test')
+    app.config['SECRET_KEY'] = 'testing'
+    app.config['MULTIPASS_AUTH_PROVIDERS'] = {'test': {'type': 'static'}}
+    app.config['MULTIPASS_IDENTITY_PROVIDERS'] = {'test': {'type': 'static'}}
+    app.config['MULTIPASS_PROVIDER_MAP'] = {'test': 'test'}
+    Multipass(app)
diff --git a/tests/test_util.py b/tests/test_util.py
index 4fc6532..250fbee 100644
--- a/tests/test_util.py
+++ b/tests/test_util.py
@@ -4,6 +4,7 @@
 # Flask-Multipass is free software; you can redistribute it
 # and/or modify it under the terms of the Revised BSD License.
 
+import sys
 from importlib.metadata import EntryPoint
 
 import pytest
@@ -170,7 +171,7 @@ def test_login_view(mocker):
 
 
 class DummyBase:
-    _entry_point = 'dummy'
+    _entry_point = 'fakeproviders'
 
 
 class Dummy(DummyBase):
@@ -190,21 +191,29 @@ class MockEntryPoint(EntryPoint):
         return mapping[self.name]
 
 
-def mock_entry_point(name):
+def mock_entry_point(name, n=0):
     # we can't override MockEntryPoint's `__init__` to pass the default values since it's using
     # a namedtuple in Python<3.11, and required args are handled via __new__ in namedtuple
-    return MockEntryPoint(name, 'dummy.value', 'dummy.group')
+    return MockEntryPoint(name, f'dummy.value.{n}', 'dummy.group')
 
 
 @pytest.fixture
 def mock_entry_points(monkeypatch):
-    def _mock_entry_points(*, group, name):
-        return {
-            'dummy': [mock_entry_point('dummy')],
-            'fake': [mock_entry_point('fake')],
-            'multi': [mock_entry_point('dummy'), mock_entry_point('fake')],
-            'unknown': []
-        }[name]
+    MOCK_EPS = {
+        'fakeproviders': [
+            mock_entry_point('dummy'),
+            mock_entry_point('fake'),
+            mock_entry_point('multi'),
+            mock_entry_point('multi', 1),
+        ]
+    }
+
+    if sys.version_info < (3, 10):
+        def _mock_entry_points():
+            return MOCK_EPS
+    else:
+        def _mock_entry_points(*, group, name):
+            return [ep for ep in MOCK_EPS[group] if ep.name == name]
 
     monkeypatch.setattr('flask_multipass.util.importlib_entry_points', _mock_entry_points)