Repository: dwavesystems/dwave-cloud-client (python)

Issue:
`dwave install` broken on py39 after utils refactor
Due to a change in `entry_points()` behavior across a few versions of python (see compatibility note in [docs](https://docs.python.org/3/library/importlib.metadata.html#entry-points)), we use `entry_points()` from `importlib_metadata` backport, instead of `importlib.metadata`.

However, during utils refactor in #628, we switched to `importlib.metadata`. This causes failure of `get_contrib_config` (used from `dwave install` and `dwave setup` CLI commands) on Python 3.9 (only!).

https://github.com/dwavesystems/dwave-cloud-client/blob/30d69493a5f3b3fdaeeda872d253d5b735ad8fa0/dwave/cloud/utils/dist.py#L36



Pull request description:
Fix entry points enumeration across python versions
Fix #631.

Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 1a16f98..7f9c558 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -213,6 +213,42 @@ jobs:
             . env/bin/activate
             make -C docs/ linkcheck
 
+  test-cli:
+    parameters:
+      python-version:
+        type: string
+
+    docker:
+      - image: cimg/python:<< parameters.python-version >>
+
+    steps:
+      - checkout
+
+      - run:
+          name: Install
+          command: |
+            python -m venv env
+            . env/bin/activate
+            pip install .
+            pip install dwave-inspector
+
+      - run:
+          name: Test contrib install
+          command: |
+            . env/bin/activate
+
+            # listing works
+            dwave install --list -v | grep -F 'Package: inspector'
+
+            # assert inspector not installed
+            ! pip show dwave-inspectorapp
+
+            # install works
+            dwave install inspector --yes
+
+            # assert inspector installed
+            pip show dwave-inspectorapp
+
   build-dist:
     docker:
       - image: cimg/python:3.11
@@ -298,6 +334,12 @@ workflows:
             parameters:
               python-version: *python-versions
 
+      - test-cli:
+          name: test-cli-linux-py<< matrix.python-version >>
+          matrix:
+            parameters:
+              python-version: *python-versions
+
       - test-docs
       - build-dist
 
diff --git a/dwave/cloud/utils/dist.py b/dwave/cloud/utils/dist.py
index 5923018..37edd92 100644
--- a/dwave/cloud/utils/dist.py
+++ b/dwave/cloud/utils/dist.py
@@ -19,9 +19,10 @@
 """
 
 from importlib.metadata import (
-    entry_points, EntryPoint, Distribution, PackageNotFoundError)
-from typing import Dict, List, OrderedDict, Union
+    EntryPoint, Distribution, PackageNotFoundError)
+from typing import List, OrderedDict, Union
 
+from importlib_metadata import entry_points
 from packaging.requirements import Requirement
 
 __all__ = ['get_distribution', 'PackageNotFoundError', 'VersionNotFoundError']
@@ -30,7 +31,7 @@ __all__ = ['get_distribution', 'PackageNotFoundError', 'VersionNotFoundError']
 def get_contrib_config() -> List[EntryPoint]:
     """Return all registered contrib (non-open-source) Ocean packages."""
 
-    # Note: we use `entry_points` from `importlib_metadata` to simplify access
+    # Note: we use `entry_points` from `importlib_metadata>=5` to simplify access
     # and use py312 semantics. See "compatibility note" in `importlib.metadata`
     # docs for entry points.
     contrib = [ep.load() for ep in entry_points(group='dwave_contrib')]
diff --git a/dwave/cloud/utils/http.py b/dwave/cloud/utils/http.py
index 0966c39..14f2ea6 100644
--- a/dwave/cloud/utils/http.py
+++ b/dwave/cloud/utils/http.py
@@ -137,7 +137,7 @@ def user_agent(name: typing.Optional[str] = None,
 
     # add platform-specific tags
     if include_platform_tags:
-        tags.extend(get_platform_tags())
+        tags.extend(platform_tags())
 
     return ' '.join("{}/{}".format(name, version) for name, version in tags)
 
@@ -149,7 +149,7 @@ def default_user_agent() -> str:
         name=__packagename__, version=__version__, include_platform_tags=False)
 
 
-def get_platform_tags() -> typing.List[str]:
+def platform_tags() -> typing.List[str]:
     """Return a list of platform tags generated from registered entry points."""
 
     # import only when needed, as it's pretty slow! (~10ms)
diff --git a/releasenotes/notes/fix-entry-points-62622b9b1d2c0190.yaml b/releasenotes/notes/fix-entry-points-62622b9b1d2c0190.yaml
new file mode 100644
index 0000000..0c9064a
--- /dev/null
+++ b/releasenotes/notes/fix-entry-points-62622b9b1d2c0190.yaml
@@ -0,0 +1,6 @@
+---
+fixes:
+  - |
+    Fix a regression bug introduced during utils module refactor and keep using
+    ``importlib_metadata`` for ``entry_points()`` access.
+    See `#631 <https://github.com/dwavesystems/dwave-cloud-client/issues/631>`_.


Test patch:
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 093a030..4529ec5 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -36,9 +36,10 @@ from parameterized import parameterized
 from dwave.cloud.utils.coders import NumpyEncoder, coerce_numpy_to_python
 from dwave.cloud.utils.cli import default_text_input
 from dwave.cloud.utils.decorators import aliasdict, cached, deprecated, retried
-from dwave.cloud.utils.dist import get_distribution, PackageNotFoundError, VersionNotFoundError
+from dwave.cloud.utils.dist import (
+    get_contrib_packages, get_distribution, PackageNotFoundError, VersionNotFoundError)
 from dwave.cloud.utils.exception import hasinstance, exception_chain, is_caused_by
-from dwave.cloud.utils.http import user_agent, default_user_agent
+from dwave.cloud.utils.http import user_agent, default_user_agent, platform_tags
 from dwave.cloud.utils.logging import (
     FilteredSecretsFormatter, configure_logging, parse_loglevel)
 from dwave.cloud.utils.qubo import (
@@ -173,6 +174,13 @@ class TestSimpleUtils(unittest.TestCase):
         for key in required:
             self.assertIn(key, ua)
 
+    def test_platform_tags_smoke(self):
+        pt = platform_tags()
+        if pt:
+            self.assertNotEqual(
+                user_agent(include_platform_tags=False),
+                user_agent(include_platform_tags=True))
+
     def test_default_user_agent(self):
         from dwave.cloud.package_info import __packagename__, __version__
         ua = default_user_agent()
@@ -979,6 +987,14 @@ class TestDistUtils(unittest.TestCase):
         with self.assertRaises(PackageNotFoundError):
             get_distribution("package-that-is-not-installed")
 
+    def test_contrib_packages_smoke(self):
+        # requires installed contrib packages, or the entry_points patcher we
+        # plan to port from dwave-inspector
+        try:
+            get_contrib_packages()
+        except:
+            self.fail("can't enumerate dwave contrib packages")
+
 
 def capture_stderr(fn):
     """Decorator that captures stderr and provides access via `output` argument."""