Repository: pytest-dev/pytest-randomly (language: python)

Issue:
DeprecationWarning with importlib-metadata>=3.9.0
`importlib-metadata` 3.6.0 changed the behaviour of `entry_points()` to no longer return a dictionary of entry points but instead return an object with a deprecated dict-like interface. As of `importlib-metadata` 3.9.0 a DeprecationWarning is emitted when calling the dictionary methods.

`pytest-randomly` calls `.get()` here:

https://github.com/pytest-dev/pytest-randomly/blob/1f0ef495da80b200123f5a58647cef2e5a42e701/src/pytest_randomly.py#L151-L156

This results in the following warning:

```python
venv/lib/python3.7/site-packages/pytest_randomly.py:153: DeprecationWarning: SelectableGroups dict interface is deprecated. Use select.
  e.load() for e in entry_points().get("pytest_randomly.random_seeder", [])
```

If warnings are turned into errors, this causes an internal error in pytest.

<details>
  <summary>INTERNALERROR</summary>

```python traceback
(venv) domdf@ryzen:/tmp/pytest_randomly_deprecation$ PYTHONDEVMODE=1 PYTHONWARNINGS=error pytest test_foo.py
============================================================================================ test session starts ============================================================================================
platform linux -- Python 3.7.10, pytest-6.2.2, py-1.10.0, pluggy-0.13.1
INTERNALERROR> Traceback (most recent call last):
INTERNALERROR>   File "/tmp/pytest_randomly_deprecation/venv/lib/python3.7/site-packages/_pytest/main.py", line 267, in wrap_session
INTERNALERROR>     config.hook.pytest_sessionstart(session=session)
INTERNALERROR>   File "/tmp/pytest_randomly_deprecation/venv/lib/python3.7/site-packages/pluggy/hooks.py", line 286, in __call__
INTERNALERROR>     return self._hookexec(self, self.get_hookimpls(), kwargs)
INTERNALERROR>   File "/tmp/pytest_randomly_deprecation/venv/lib/python3.7/site-packages/pluggy/manager.py", line 93, in _hookexec
INTERNALERROR>     return self._inner_hookexec(hook, methods, kwargs)
INTERNALERROR>   File "/tmp/pytest_randomly_deprecation/venv/lib/python3.7/site-packages/pluggy/manager.py", line 87, in <lambda>
INTERNALERROR>     firstresult=hook.spec.opts.get("firstresult") if hook.spec else False,
INTERNALERROR>   File "/tmp/pytest_randomly_deprecation/venv/lib/python3.7/site-packages/pluggy/callers.py", line 208, in _multicall
INTERNALERROR>     return outcome.get_result()
INTERNALERROR>   File "/tmp/pytest_randomly_deprecation/venv/lib/python3.7/site-packages/pluggy/callers.py", line 80, in get_result
INTERNALERROR>     raise ex[1].with_traceback(ex[2])
INTERNALERROR>   File "/tmp/pytest_randomly_deprecation/venv/lib/python3.7/site-packages/pluggy/callers.py", line 187, in _multicall
INTERNALERROR>     res = hook_impl.function(*args)
INTERNALERROR>   File "/tmp/pytest_randomly_deprecation/venv/lib/python3.7/site-packages/_pytest/terminal.py", line 713, in pytest_sessionstart
INTERNALERROR>     config=self.config, startdir=self.startdir
INTERNALERROR>   File "/tmp/pytest_randomly_deprecation/venv/lib/python3.7/site-packages/pluggy/hooks.py", line 286, in __call__
INTERNALERROR>     return self._hookexec(self, self.get_hookimpls(), kwargs)
INTERNALERROR>   File "/tmp/pytest_randomly_deprecation/venv/lib/python3.7/site-packages/pluggy/manager.py", line 93, in _hookexec
INTERNALERROR>     return self._inner_hookexec(hook, methods, kwargs)
INTERNALERROR>   File "/tmp/pytest_randomly_deprecation/venv/lib/python3.7/site-packages/pluggy/manager.py", line 87, in <lambda>
INTERNALERROR>     firstresult=hook.spec.opts.get("firstresult") if hook.spec else False,
INTERNALERROR>   File "/tmp/pytest_randomly_deprecation/venv/lib/python3.7/site-packages/pluggy/callers.py", line 208, in _multicall
INTERNALERROR>     return outcome.get_result()
INTERNALERROR>   File "/tmp/pytest_randomly_deprecation/venv/lib/python3.7/site-packages/pluggy/callers.py", line 80, in get_result
INTERNALERROR>     raise ex[1].with_traceback(ex[2])
INTERNALERROR>   File "/tmp/pytest_randomly_deprecation/venv/lib/python3.7/site-packages/pluggy/callers.py", line 187, in _multicall
INTERNALERROR>     res = hook_impl.function(*args)
INTERNALERROR>   File "/tmp/pytest_randomly_deprecation/venv/lib/python3.7/site-packages/pytest_randomly.py", line 170, in pytest_report_header
INTERNALERROR>     _reseed(config)
INTERNALERROR>   File "/tmp/pytest_randomly_deprecation/venv/lib/python3.7/site-packages/pytest_randomly.py", line 153, in _reseed
INTERNALERROR>     e.load() for e in entry_points().get("pytest_randomly.random_seeder", [])
INTERNALERROR>   File "/tmp/pytest_randomly_deprecation/venv/lib/python3.7/site-packages/importlib_metadata/__init__.py", line 302, in get
INTERNALERROR>     flake8_bypass(self._warn)()
INTERNALERROR> DeprecationWarning: SelectableGroups dict interface is deprecated. Use select.

```  

</details>


See https://github.com/python/importlib_metadata/pull/278 and https://github.com/python/importlib_metadata/pull/289 for more context.

This is currently only an issue on Python 3.7 and earlier, as `pytest-randomly` uses the standard library on newer versions.


Discussion / hints:
The changelog suggests https://pypi.org/project/backports.entry-points-selectable/ as a possible solution.

Fix patch:
diff --git a/HISTORY.rst b/HISTORY.rst
index ad5229b..0b118a2 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -2,6 +2,11 @@
 History
 =======
 
+* Fix deprecation warning from importlib-metadata 3.9.0+.
+
+  Thanks to Dominic Davis-Foster for report in `Issue #333
+  <https://github.com/pytest-dev/pytest-randomly/issue/333>`__.
+
 * Stop distributing tests to reduce package size. Tests are not intended to be
   run outside of the tox setup in the repository. Repackagers can use GitHub's
   tarballs per tag.
diff --git a/requirements/py36.txt b/requirements/py36.txt
index 0652916..6857aa7 100644
--- a/requirements/py36.txt
+++ b/requirements/py36.txt
@@ -12,6 +12,10 @@ attrs==20.3.0 \
     --hash=sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6 \
     --hash=sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700
     # via pytest
+backports.entry-points-selectable==1.0.2 ; python_version < "3.10" \
+    --hash=sha256:1817925560c765efe73fb692c047fb007511388d53e6dbeec7bfcda391eb83ac \
+    --hash=sha256:98b9fda50dab4f298f646bc7b717b6a3b90dd4d509015f1ce9e66b41a1330d46
+    # via -r requirements.in
 execnet==1.8.0 \
     --hash=sha256:7a13113028b1e1cc4c6492b28098b3c6576c9dccc7973bfe47b342afadafb2ac \
     --hash=sha256:b73c5565e517f24b62dea8a5ceac178c661c4309d3aa0c3e420856c072c411b4
@@ -26,11 +30,11 @@ faker==6.6.3 \
     # via
     #   -r requirements.in
     #   factory-boy
-importlib-metadata==3.8.1 ; python_version < "3.8" \
+importlib-metadata==3.8.1 \
     --hash=sha256:eb83dfc8d87a64d42fb2904312d632bb8d57e47c5b435f8d4d5523d2b0efc3d0 \
     --hash=sha256:fc605f818ff2389df65d893ccefcd5310678c48c2855c42619464ed48556ff08
     # via
-    #   -r requirements.in
+    #   backports.entry-points-selectable
     #   pluggy
     #   pytest
 iniconfig==1.1.1 \
diff --git a/requirements/py37.txt b/requirements/py37.txt
index c106882..bca1c04 100644
--- a/requirements/py37.txt
+++ b/requirements/py37.txt
@@ -12,6 +12,10 @@ attrs==20.3.0 \
     --hash=sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6 \
     --hash=sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700
     # via pytest
+backports.entry-points-selectable==1.0.2 ; python_version < "3.10" \
+    --hash=sha256:1817925560c765efe73fb692c047fb007511388d53e6dbeec7bfcda391eb83ac \
+    --hash=sha256:98b9fda50dab4f298f646bc7b717b6a3b90dd4d509015f1ce9e66b41a1330d46
+    # via -r requirements.in
 execnet==1.8.0 \
     --hash=sha256:7a13113028b1e1cc4c6492b28098b3c6576c9dccc7973bfe47b342afadafb2ac \
     --hash=sha256:b73c5565e517f24b62dea8a5ceac178c661c4309d3aa0c3e420856c072c411b4
@@ -26,11 +30,11 @@ faker==6.6.3 \
     # via
     #   -r requirements.in
     #   factory-boy
-importlib-metadata==3.8.1 ; python_version < "3.8" \
+importlib-metadata==3.8.1 \
     --hash=sha256:eb83dfc8d87a64d42fb2904312d632bb8d57e47c5b435f8d4d5523d2b0efc3d0 \
     --hash=sha256:fc605f818ff2389df65d893ccefcd5310678c48c2855c42619464ed48556ff08
     # via
-    #   -r requirements.in
+    #   backports.entry-points-selectable
     #   pluggy
     #   pytest
 iniconfig==1.1.1 \
diff --git a/requirements/py38.txt b/requirements/py38.txt
index 56cda44..85c1676 100644
--- a/requirements/py38.txt
+++ b/requirements/py38.txt
@@ -12,6 +12,10 @@ attrs==20.3.0 \
     --hash=sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6 \
     --hash=sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700
     # via pytest
+backports.entry-points-selectable==1.0.2 ; python_version < "3.10" \
+    --hash=sha256:1817925560c765efe73fb692c047fb007511388d53e6dbeec7bfcda391eb83ac \
+    --hash=sha256:98b9fda50dab4f298f646bc7b717b6a3b90dd4d509015f1ce9e66b41a1330d46
+    # via -r requirements.in
 execnet==1.8.0 \
     --hash=sha256:7a13113028b1e1cc4c6492b28098b3c6576c9dccc7973bfe47b342afadafb2ac \
     --hash=sha256:b73c5565e517f24b62dea8a5ceac178c661c4309d3aa0c3e420856c072c411b4
diff --git a/requirements/py39.txt b/requirements/py39.txt
index 56cda44..85c1676 100644
--- a/requirements/py39.txt
+++ b/requirements/py39.txt
@@ -12,6 +12,10 @@ attrs==20.3.0 \
     --hash=sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6 \
     --hash=sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700
     # via pytest
+backports.entry-points-selectable==1.0.2 ; python_version < "3.10" \
+    --hash=sha256:1817925560c765efe73fb692c047fb007511388d53e6dbeec7bfcda391eb83ac \
+    --hash=sha256:98b9fda50dab4f298f646bc7b717b6a3b90dd4d509015f1ce9e66b41a1330d46
+    # via -r requirements.in
 execnet==1.8.0 \
     --hash=sha256:7a13113028b1e1cc4c6492b28098b3c6576c9dccc7973bfe47b342afadafb2ac \
     --hash=sha256:b73c5565e517f24b62dea8a5ceac178c661c4309d3aa0c3e420856c072c411b4
diff --git a/requirements/requirements.in b/requirements/requirements.in
index ef4c3e6..704d2ab 100644
--- a/requirements/requirements.in
+++ b/requirements/requirements.in
@@ -1,6 +1,6 @@
+backports.entry-points-selectable ; python_version < "3.10"
 factory_boy
 faker
-importlib-metadata ; python_version < "3.8"
 numpy
 pytest
 pytest-xdist
diff --git a/setup.cfg b/setup.cfg
index af1d665..8ffc1c6 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -32,7 +32,7 @@ package_dir=
 py_modules = pytest_randomly
 include_package_data = True
 install_requires =
-    importlib-metadata ; python_version < "3.8"
+    backports.entry-points-selectable ; python_version < "3.10"
     pytest
 python_requires = >=3.6
 zip_safe = False


Test patch:
diff --git a/src/pytest_randomly.py b/src/pytest_randomly.py
index 14062d3..9064c27 100644
--- a/src/pytest_randomly.py
+++ b/src/pytest_randomly.py
@@ -1,15 +1,11 @@
 import argparse
 import hashlib
 import random
-import sys
 
+# if sys.version_info < (3, 10):
+from backports.entry_points_selectable import entry_points
 from pytest import Collector, fixture
 
-if sys.version_info >= (3, 8):
-    from importlib.metadata import entry_points
-else:
-    from importlib_metadata import entry_points
-
 try:
     import xdist
 except ImportError:
@@ -150,7 +146,8 @@ def _reseed(config, offset=0):
 
     if entrypoint_reseeds is None:
         entrypoint_reseeds = [
-            e.load() for e in entry_points().get("pytest_randomly.random_seeder", [])
+            e.load()
+            for e in entry_points().select(group="pytest_randomly.random_seeder")
         ]
     for reseed in entrypoint_reseeds:
         reseed(seed)
diff --git a/tests/test_pytest_randomly.py b/tests/test_pytest_randomly.py
index 07cb646..cfdee9b 100644
--- a/tests/test_pytest_randomly.py
+++ b/tests/test_pytest_randomly.py
@@ -1,3 +1,4 @@
+from types import SimpleNamespace
 from unittest.mock import Mock
 
 import pytest
@@ -675,10 +676,10 @@ def load(self):
     entry_points = []
 
     def fake_entry_points():
-        return {
-            "pytest_randomly.random_seeder": entry_points,
-            "ignore": lambda x: 1 / 0,
-        }
+        return SimpleNamespace(select=fake_select)
+
+    def fake_select(*, group):
+        return entry_points
 
     monkeypatch.setattr(pytest_randomly, "entry_points", fake_entry_points)
     reseed = Mock()