Repository: PennyLaneAI/pennylane
Pull request #4783 (2023-11-03 13:36:06)

use importlib instead of pkg_resources
**Context:**
`pkg_resources` is part of setuptools, which is being replaced by `importlib` (see warning on [the setuptools page](https://setuptools.pypa.io/en/latest/pkg_resources.html)). Using importlib (part of standard python) relates to implementing PEP517/518, since setuptools is the old way and shouldn't be assumed to be around anymore.

**Description of the Change:**
Replace any uses of `pkg_resources` with `importlib.metadata`. The main switch:
- Before: `entries = pkg_resources.iter_entry_points("pennylane.plugins")`
- Now (python <3.10): `entries = importlib.metadata.entry_points()["pennylane.plugins"]`
- Now (python >=3.10): `entries = importlib.metadata.entry_points(group="pennylane.plugins")`

**Benefits:**
We're replacing the old, deprecated system with the new one.

**Possible Drawbacks:**
- The support for python 3.9 is ugly, but should be tidied once we drop 3.9 support (the importlib people know this - they have a [See also](https://docs.python.org/3/library/importlib.metadata.html) that suggests using a 3P library called `importlib_metadata` which handles the backporting nicely)
- entrypoints show up twice with editable PL installs. See pypa/setuptools#3649 for explanation (seems like a wont-fix)

github-actions[bot]: Hello. You may have forgotten to update the changelog!
Please edit [`doc/releases/changelog-dev.md`](/PennyLaneAI/pennylane/blob/master/doc/releases/changelog-dev.md) with:
* A one-to-two sentence description of the change. You may include a small working example for new features.
* A link back to this PR.
* Your name (or GitHub username) in the contributors section.
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4783?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Comparison is base [(`e8ac0a1`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/e8ac0a1d7eb30f2812bd2ab273803640dc1ab837?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) 99.64% compared to head [(`8b379a9`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4783?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) 99.64%.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #4783      +/-   ##
==========================================
- Coverage   99.64%   99.64%   -0.01%     
==========================================
  Files         381      381              
  Lines       34302    34301       -1     
==========================================
- Hits        34181    34180       -1     
  Misses        121      121              
```


| [Files](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4783?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | Coverage Δ | |
|---|---|---|
| [pennylane/\_\_init\_\_.py](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4783?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL19faW5pdF9fLnB5) | `100.00% <100.00%> (ø)` | |
| [pennylane/about.py](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4783?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2Fib3V0LnB5) | `100.00% <100.00%> (ø)` | |
| [pennylane/drawer/draw.py](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4783?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2RyYXdlci9kcmF3LnB5) | `100.00% <100.00%> (ø)` | |
| [pennylane/io.py](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4783?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2lvLnB5) | `100.00% <100.00%> (ø)` | |


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4783?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).


Patch:
diff --git a/doc/code/qml.rst b/doc/code/qml.rst
index 966eb0742..fa32159fe 100644
--- a/doc/code/qml.rst
+++ b/doc/code/qml.rst
@@ -6,4 +6,4 @@ qml
 .. automodapi:: pennylane
     :no-heading:
     :include-all-objects:
-    :skip: iter_entry_points, Version, SimpleSpec, plugin_devices, plugin_converters, default_config, reload
+    :skip: Version, SimpleSpec, plugin_devices, plugin_converters, default_config, reload, version_info, defaultdict
diff --git a/pennylane/__init__.py b/pennylane/__init__.py
index 29932e98b..9a3398a3d 100644
--- a/pennylane/__init__.py
+++ b/pennylane/__init__.py
@@ -15,10 +15,8 @@
 This is the top level module from which all basic functions and classes of
 PennyLane can be directly imported.
 """
-from importlib import reload
-import types
-import warnings
-import pkg_resources
+from importlib import reload, metadata
+from sys import version_info
 
 
 import numpy as _np
@@ -144,7 +142,13 @@ class QuantumFunctionError(Exception):
 def _get_device_entrypoints():
     """Returns a dictionary mapping the device short name to the
     loadable entrypoint"""
-    return {entry.name: entry for entry in pkg_resources.iter_entry_points("pennylane.plugins")}
+    entries = (
+        metadata.entry_points()["pennylane.plugins"]
+        if version_info[:2] == (3, 9)
+        # pylint:disable=unexpected-keyword-arg
+        else metadata.entry_points(group="pennylane.plugins")
+    )
+    return {entry.name: entry for entry in entries}
 
 
 def refresh_devices():
@@ -154,11 +158,11 @@ def refresh_devices():
     # which is to update the global plugin_devices variable.
 
     # We wish to retain the behaviour of a global plugin_devices dictionary,
-    # as re-importing pkg_resources can be a very slow operation on systems
+    # as re-importing metadata can be a very slow operation on systems
     # with a large number of installed packages.
     global plugin_devices  # pylint:disable=global-statement
 
-    reload(pkg_resources)
+    reload(metadata)
     plugin_devices = _get_device_entrypoints()
 
 
diff --git a/pennylane/about.py b/pennylane/about.py
index 2f932472c..ba2a83b58 100644
--- a/pennylane/about.py
+++ b/pennylane/about.py
@@ -18,7 +18,8 @@ e.g., OS, version, `Numpy` and `Scipy` versions, installation method.
 import platform
 import sys
 from subprocess import check_output
-from pkg_resources import iter_entry_points
+from importlib import metadata
+from sys import version_info
 import numpy
 import scipy
 
@@ -27,7 +28,16 @@ def about():
     """
     Prints the information for pennylane installation.
     """
-    plugin_devices = iter_entry_points("pennylane.plugins")
+    if version_info[:2] == (3, 9):
+        from pkg_resources import iter_entry_points  # pylint:disable=import-outside-toplevel
+
+        plugin_devices = iter_entry_points("pennylane.plugins")
+        dist_name = "project_name"
+    else:
+        plugin_devices = metadata.entry_points(  # pylint:disable=unexpected-keyword-arg
+            group="pennylane.plugins"
+        )
+        dist_name = "name"
     print(check_output([sys.executable, "-m", "pip", "show", "pennylane"]).decode())
     print(f"Platform info:           {platform.platform(aliased=True)}")
     print(
@@ -39,7 +49,7 @@ def about():
     print("Installed devices:")
 
     for d in plugin_devices:
-        print(f"- {d.name} ({d.dist.project_name}-{d.dist.version})")
+        print(f"- {d.name} ({getattr(d.dist, dist_name)}-{d.dist.version})")
 
 
 if __name__ == "__main__":
diff --git a/pennylane/drawer/draw.py b/pennylane/drawer/draw.py
index b43e53f4b..545781816 100644
--- a/pennylane/drawer/draw.py
+++ b/pennylane/drawer/draw.py
@@ -17,8 +17,8 @@
 Contains the drawing function.
 """
 from functools import wraps
+from importlib.metadata import distribution
 import warnings
-import pkg_resources
 
 import pennylane as qml
 from .tape_mpl import tape_mpl
@@ -28,9 +28,9 @@ from .tape_text import tape_text
 def catalyst_qjit(qnode):
     """The ``catalyst.while`` wrapper method"""
     try:
-        pkg_resources.get_distribution("pennylane_catalyst")
+        distribution("pennylane_catalyst")
         return qnode.__class__.__name__ == "QJIT"
-    except pkg_resources.DistributionNotFound:
+    except ImportError:
         return False
 
 
diff --git a/pennylane/io.py b/pennylane/io.py
index 159159466..5e2f3b436 100644
--- a/pennylane/io.py
+++ b/pennylane/io.py
@@ -15,10 +15,17 @@
 This module contains functions to load circuits from other frameworks as
 PennyLane templates.
 """
-from pkg_resources import iter_entry_points
+from collections import defaultdict
+from importlib import metadata
+from sys import version_info
 
 # get list of installed plugin converters
-plugin_converters = {entry.name: entry for entry in iter_entry_points("pennylane.io")}
+__plugin_devices = (
+    defaultdict(tuple, metadata.entry_points())["pennylane.io"]
+    if version_info[:2] == (3, 9)
+    else metadata.entry_points(group="pennylane.io")  # pylint:disable=unexpected-keyword-arg
+)
+plugin_converters = {entry.name: entry for entry in __plugin_devices}
 
 
 def load(quantum_circuit_object, format: str):


Test patch:
diff --git a/.github/workflows/interface-unit-tests.yml b/.github/workflows/interface-unit-tests.yml
index 42890ea19..a0cfc87bd 100644
--- a/.github/workflows/interface-unit-tests.yml
+++ b/.github/workflows/interface-unit-tests.yml
@@ -74,7 +74,7 @@ jobs:
             "qcut-tests": ["3.9"],
             "qchem-tests": ["3.9"],
             "gradients-tests": ["3.9"],
-            "data-tests": ["3.10"],
+            "data-tests": ["3.9", "3.10"],
             "device-tests": ["3.9"]
           }
           EOF
diff --git a/tests/test_device.py b/tests/test_device.py
index d56f6b0d7..4ca0d21a5 100644
--- a/tests/test_device.py
+++ b/tests/test_device.py
@@ -14,9 +14,9 @@
 """
 Unit tests for the :mod:`pennylane` :class:`Device` class.
 """
-import importlib
+from importlib import metadata, reload
 from collections import OrderedDict
-import pkg_resources
+from sys import version_info
 
 import pytest
 import numpy as np
@@ -933,17 +933,17 @@ class TestDeviceInit:
             with pytest.raises(DeviceError, match="plugin requires PennyLane versions"):
                 qml.device("default.qubit.legacy", wires=0)
 
-    @pytest.mark.skip(reason="Reloading PennyLane messes with tape mode")
     def test_refresh_entrypoints(self, monkeypatch):
         """Test that new entrypoints are found by the refresh_devices function"""
         assert qml.plugin_devices
 
         with monkeypatch.context() as m:
             # remove all entry points
-            m.setattr(pkg_resources, "iter_entry_points", lambda name: [])
+            retval = {"pennylane.plugins": []} if version_info[:2] == (3, 9) else []
+            m.setattr(metadata, "entry_points", lambda **kwargs: retval)
 
             # reimporting PennyLane within the context sets qml.plugin_devices to {}
-            importlib.reload(qml)
+            reload(qml)
 
             # since there are no entry points, there will be no plugin devices
             assert not qml.plugin_devices
@@ -955,36 +955,36 @@ class TestDeviceInit:
 
         # Test teardown: re-import PennyLane to revert all changes and
         # restore the plugin_device dictionary
-        importlib.reload(qml)
+        reload(qml)
 
-    @pytest.mark.skip(reason="Reloading PennyLane messes with tape mode")
     def test_hot_refresh_entrypoints(self, monkeypatch):
         """Test that new entrypoints are found by the device loader if not currently present"""
         assert qml.plugin_devices
 
         with monkeypatch.context() as m:
             # remove all entry points
-            m.setattr(pkg_resources, "iter_entry_points", lambda name: [])
+            retval = {"pennylane.plugins": []} if version_info[:2] == (3, 9) else []
+            m.setattr(metadata, "entry_points", lambda **kwargs: retval)
 
             # reimporting PennyLane within the context sets qml.plugin_devices to {}
-            importlib.reload(qml)
+            reload(qml)
 
             m.setattr(qml, "refresh_devices", lambda: None)
             assert not qml.plugin_devices
 
             # since there are no entry points, there will be no plugin devices
-            with pytest.raises(DeviceError, match="Device does not exist"):
+            with pytest.raises(DeviceError, match="Device default.qubit does not exist"):
                 qml.device("default.qubit", wires=0)
 
         # outside of the context, entrypoints will now be found automatically
         assert not qml.plugin_devices
         dev = qml.device("default.qubit", wires=0)
         assert qml.plugin_devices
-        assert dev.short_name == "default.qubit"
+        assert dev.name == "default.qubit"
 
         # Test teardown: re-import PennyLane to revert all changes and
         # restore the plugin_device dictionary
-        importlib.reload(qml)
+        reload(qml)
 
     def test_shot_vector_property(self):
         """Tests shot vector initialization."""