Repository: open-telemetry/opentelemetry-python

Issue:
Make entry_points behave the same across Python versions
The recently introduced `entry_points` function does not behave the same across Python versions and it is not possible to get all entry points in Python 3.8 and 3.9.


Discussion:
@ocelotl 

Do you have an example in which this does not work in 3.8 and 3.9 so we can try to reproduce?
> @ocelotl
> 
> Do you have an example in which this does not work in 3.8 and 3.9 so we can try to reproduce?

Ok, so we have 2 issues:

1. We may need to go through all the entry points in some scenario, the current implementation of our [`entry_points`](https://github.com/open-telemetry/opentelemetry-python/blob/main/opentelemetry-api/src/opentelemetry/util/_importlib_metadata.py#L30) function requires both `group` and `name` which a user who wants all entry points won't know, that's why all the implementations of the functions that give entry points in `importlib` or `importlib_metadata` have both `group` or `name` or their equivalents as optional arguments.
2. The replacements of `pkg_resources` functions that return entry points in `importlib` or `importlib_metadata` are very inconsistent across Python versions, for example [3.8](https://docs.python.org/3.8/library/importlib.metadata.html#entry-points) has an `entry_points` function that returns a dictionary, [3.10](https://docs.python.org/3.10/library/importlib.metadata.html#entry-points) returns a collection of entry point objects.

The idea behind this PR is to solve both issues by creating a private module that we can use in OTel components to access entry points that provides an uniform API and allows for selecting entry points by `group`, `name` or none of them.
@ocelotl I have a high level question. Rather than add our own wrapper library, could we just use the `importlib-metadata` package instead regardless of version? Right now we have this conditional requirement

https://github.com/open-telemetry/opentelemetry-python/blob/f05fa772e65b72a1fdc0a40e142e9342dee013f3/opentelemetry-api/pyproject.toml#L30

Once we drop Python 3.9 in the distant future, we could move to stdlib `importlib.metadata`. Are there any downsides to this?

Resolving patch:
diff --git a/opentelemetry-api/src/opentelemetry/util/_importlib_metadata.py b/opentelemetry-api/src/opentelemetry/util/_importlib_metadata.py
--- a/opentelemetry-api/src/opentelemetry/util/_importlib_metadata.py
+++ b/opentelemetry-api/src/opentelemetry/util/_importlib_metadata.py
@@ -12,27 +12,18 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from sys import version_info
+# FIXME: Use importlib.metadata when support for 3.11 is dropped if the rest of
+# the supported versions at that time have the same API.
+from importlib_metadata import (  # type: ignore
+    EntryPoint,
+    EntryPoints,
+    entry_points,
+    version,
+)
 
-# FIXME remove this when support for 3.7 is dropped.
-if version_info.minor == 7:
-    # pylint: disable=import-error
-    from importlib_metadata import entry_points, version  # type: ignore
+# The importlib-metadata library has introduced breaking changes before to its
+# API, this module is kept just to act as a layer between the
+# importlib-metadata library and our project if in any case it is necessary to
+# do so.
 
-# FIXME remove this file when support for 3.9 is dropped.
-elif version_info.minor in (8, 9):
-    # pylint: disable=import-error
-    from importlib.metadata import (
-        entry_points as importlib_metadata_entry_points,
-    )
-    from importlib.metadata import version
-
-    def entry_points(group: str, name: str):  # type: ignore
-        for entry_point in importlib_metadata_entry_points()[group]:
-            if entry_point.name == name:
-                yield entry_point
-
-else:
-    from importlib.metadata import entry_points, version
-
-__all__ = ["entry_points", "version"]
+__all__ = ["entry_points", "version", "EntryPoint", "EntryPoints"]


Test patch:
diff --git a/opentelemetry-api/tests/util/test__importlib_metadata.py b/opentelemetry-api/tests/util/test__importlib_metadata.py
--- a/opentelemetry-api/tests/util/test__importlib_metadata.py
+++ b/opentelemetry-api/tests/util/test__importlib_metadata.py
@@ -15,7 +15,10 @@
 from unittest import TestCase
 
 from opentelemetry.metrics import MeterProvider
-from opentelemetry.util._importlib_metadata import entry_points
+from opentelemetry.util._importlib_metadata import EntryPoint, EntryPoints
+from opentelemetry.util._importlib_metadata import (
+    entry_points as importlib_metadata_entry_points,
+)
 
 
 class TestEntryPoints(TestCase):
@@ -24,7 +27,7 @@ def test_entry_points(self):
         self.assertIsInstance(
             next(
                 iter(
-                    entry_points(
+                    importlib_metadata_entry_points(
                         group="opentelemetry_meter_provider",
                         name="default_meter_provider",
                     )
@@ -32,3 +35,74 @@ def test_entry_points(self):
             ).load()(),
             MeterProvider,
         )
+
+    def test_uniform_behavior(self):
+        """
+        Test that entry_points behaves the same regardless of the Python
+        version.
+        """
+
+        entry_points = importlib_metadata_entry_points()
+
+        self.assertIsInstance(entry_points, EntryPoints)
+
+        entry_points = entry_points.select(group="opentelemetry_propagator")
+        self.assertIsInstance(entry_points, EntryPoints)
+
+        entry_points = entry_points.select(name="baggage")
+        self.assertIsInstance(entry_points, EntryPoints)
+
+        entry_point = next(iter(entry_points))
+        self.assertIsInstance(entry_point, EntryPoint)
+
+        self.assertEqual(entry_point.name, "baggage")
+        self.assertEqual(entry_point.group, "opentelemetry_propagator")
+        self.assertEqual(
+            entry_point.value,
+            "opentelemetry.baggage.propagation:W3CBaggagePropagator",
+        )
+
+        entry_points = importlib_metadata_entry_points(
+            group="opentelemetry_propagator"
+        )
+        self.assertIsInstance(entry_points, EntryPoints)
+
+        entry_points = entry_points.select(name="baggage")
+        self.assertIsInstance(entry_points, EntryPoints)
+
+        entry_point = next(iter(entry_points))
+        self.assertIsInstance(entry_point, EntryPoint)
+
+        self.assertEqual(entry_point.name, "baggage")
+        self.assertEqual(entry_point.group, "opentelemetry_propagator")
+        self.assertEqual(
+            entry_point.value,
+            "opentelemetry.baggage.propagation:W3CBaggagePropagator",
+        )
+
+        entry_points = importlib_metadata_entry_points(name="baggage")
+        self.assertIsInstance(entry_points, EntryPoints)
+
+        entry_point = next(iter(entry_points))
+        self.assertIsInstance(entry_point, EntryPoint)
+
+        self.assertEqual(entry_point.name, "baggage")
+        self.assertEqual(entry_point.group, "opentelemetry_propagator")
+        self.assertEqual(
+            entry_point.value,
+            "opentelemetry.baggage.propagation:W3CBaggagePropagator",
+        )
+
+        entry_points = importlib_metadata_entry_points(group="abc")
+        self.assertIsInstance(entry_points, EntryPoints)
+        self.assertEqual(len(entry_points), 0)
+
+        entry_points = importlib_metadata_entry_points(
+            group="opentelemetry_propagator", name="abc"
+        )
+        self.assertIsInstance(entry_points, EntryPoints)
+        self.assertEqual(len(entry_points), 0)
+
+        entry_points = importlib_metadata_entry_points(group="abc", name="abc")
+        self.assertIsInstance(entry_points, EntryPoints)
+        self.assertEqual(len(entry_points), 0)