diff --git a/opentelemetry-api/pyproject.toml b/opentelemetry-api/pyproject.toml index 36b5fb06a58..5a6c0c2c9de 100644 --- a/opentelemetry-api/pyproject.toml +++ b/opentelemetry-api/pyproject.toml @@ -27,9 +27,7 @@ classifiers = [ ] dependencies = [ "Deprecated >= 1.2.6", - # FIXME This should be able to be removed after 3.12 is released if there is a reliable API - # in importlib.metadata. - "importlib-metadata >= 6.0, <= 8.4.0", + "importlib-metadata >= 6.0, <= 8.4.0; python_version < '3.10'", ] dynamic = [ "version", diff --git a/opentelemetry-api/src/opentelemetry/util/_importlib_metadata.py b/opentelemetry-api/src/opentelemetry/util/_importlib_metadata.py index cbf09f3ef8c..2c4fed6abb2 100644 --- a/opentelemetry-api/src/opentelemetry/util/_importlib_metadata.py +++ b/opentelemetry-api/src/opentelemetry/util/_importlib_metadata.py @@ -12,18 +12,92 @@ # See the License for the specific language governing permissions and # limitations under the License. -# 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, -) - # 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. -__all__ = ["entry_points", "version", "EntryPoint", "EntryPoints"] +from sys import version_info + +if version_info < (3, 10): + + # pylint: disable=import-error + from importlib_metadata import version # type: ignore + from importlib_metadata import ( + Distribution, + EntryPoint, + EntryPoints, + PackageNotFoundError, + distributions, + ) + from importlib_metadata import ( + entry_points as importlib_metadata_entry_points, + ) + from importlib_metadata import requires + +else: + + from importlib.metadata import ( # type: ignore + Distribution, + EntryPoint, + EntryPoints, + PackageNotFoundError, + distributions, + ) + from importlib.metadata import ( + entry_points as importlib_metadata_entry_points, + ) + + from importlib.metadata import requires, version # isort: skip + + +def entry_points(**params) -> EntryPoints: # type: ignore + """ + Same as entry_points but requires at least one argument + + For Python 3.8 or 3.9: + + isinstance( + importlib_metadata.entry_points(), importlib_metadata.EntryPoints + ) + + evaluates to True. + + For Python 3.10, 3.11: + + isinstance( + importlib.metadata.entry_points(), importlib.metadata.SelectableGroups + ) + + evaluates to True. + + For Python 3.12: + + isinstance( + importlib.metadata.entry_points(), importlib.metadata.EntryPoints + ) + + evaluates to True. + + So, when called with no arguments, entry_points returns objects of + different types depending on the Python version that is being used. This is + obviously very problematic. Nevertheless, in our code we don't ever call + entry_points without arguments, so the approach here is to redefine + entry_points so that it requires at least one argument. + """ + + if not params: # type: ignore + raise ValueError("entry_points requires at least one argument") + return importlib_metadata_entry_points(**params) # type: ignore + + +__all__ = [ + "entry_points", + "version", + "EntryPoint", + "EntryPoints", + "requires", + "Distribution", + "distributions", + "PackageNotFoundError", +] diff --git a/opentelemetry-api/test-requirements.txt b/opentelemetry-api/test-requirements-0.txt similarity index 100% rename from opentelemetry-api/test-requirements.txt rename to opentelemetry-api/test-requirements-0.txt diff --git a/opentelemetry-api/test-requirements-1.txt b/opentelemetry-api/test-requirements-1.txt new file mode 100644 index 00000000000..c2fe34e2233 --- /dev/null +++ b/opentelemetry-api/test-requirements-1.txt @@ -0,0 +1,15 @@ +asgiref==3.7.2 +Deprecated==1.2.14 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +tomli==2.0.1 +typing_extensions==4.10.0 +wrapt==1.16.0 +zipp==3.19.2 +-e opentelemetry-sdk +-e opentelemetry-semantic-conventions +-e tests/opentelemetry-test-utils +-e opentelemetry-api diff --git a/opentelemetry-api/tests/util/test__importlib_metadata.py b/opentelemetry-api/tests/util/test__importlib_metadata.py index 92a4e7dd62a..fed64335741 100644 --- a/opentelemetry-api/tests/util/test__importlib_metadata.py +++ b/opentelemetry-api/tests/util/test__importlib_metadata.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from sys import version_info from unittest import TestCase from opentelemetry.metrics import MeterProvider @@ -19,6 +20,38 @@ from opentelemetry.util._importlib_metadata import ( entry_points as importlib_metadata_entry_points, ) +from opentelemetry.util._importlib_metadata import version + + +class TestDependencies(TestCase): + + def test_dependencies(self): + + # pylint: disable=import-outside-toplevel + # pylint: disable=unused-import + # pylint: disable=import-error + if version_info < (3, 10): + try: + import importlib_metadata # type: ignore + + except ImportError: + self.fail( + "importlib_metadata not installed when testing with " + f"{version_info.major}.{version_info.minor}" + ) + + else: + try: + import importlib_metadata # noqa + + except ImportError: + pass + + else: + self.fail( + "importlib_metadata installed when testing with " + f"{version_info.major}.{version_info.minor}" + ) class TestEntryPoints(TestCase): @@ -40,27 +73,29 @@ 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) + importlib.metadata was introduced in 3.8 as a replacement for + pkg_resources. The problem is that the API of importlib.metadata + changed in subsequent versions. - entry_points = entry_points.select(group="opentelemetry_propagator") - self.assertIsInstance(entry_points, EntryPoints) + For example, in 3.8 or 3.9 importlib.metadata.entry_points does not + support the keyword arguments group or name, but those keyword + arguments are supported in 3.10, 3.11 and 3.12. - entry_points = entry_points.select(name="baggage") - self.assertIsInstance(entry_points, EntryPoints) + There exists a package named importlib-metadata that has an API that + includes a function named importlib_metadata.entry_points which + supports the keyword arguments group and name, so we use + importlib_metadata.entry_points when running in 3.8 or 3.9. - entry_point = next(iter(entry_points)) - self.assertIsInstance(entry_point, EntryPoint) + importlib_metadata.entry_points and importlib.metadata.entry_points do + not return objects of the same type when called without any arguments. + That is why the implementation of the + opentelemetry.util._importlib_metadata redefines entry_points so that + it is mandatory to use an argument. + """ - self.assertEqual(entry_point.name, "baggage") - self.assertEqual(entry_point.group, "opentelemetry_propagator") - self.assertEqual( - entry_point.value, - "opentelemetry.baggage.propagation:W3CBaggagePropagator", - ) + with self.assertRaises(ValueError): + importlib_metadata_entry_points() entry_points = importlib_metadata_entry_points( group="opentelemetry_propagator" @@ -106,3 +141,5 @@ def test_uniform_behavior(self): entry_points = importlib_metadata_entry_points(group="abc", name="abc") self.assertIsInstance(entry_points, EntryPoints) self.assertEqual(len(entry_points), 0) + + self.assertIsInstance(version("opentelemetry-api"), str) diff --git a/tox.ini b/tox.ini index 23f5947c4bd..429c1e3cb9b 100644 --- a/tox.ini +++ b/tox.ini @@ -134,7 +134,10 @@ commands_pre = mypy,mypyinstalled: pip install -r {toxinidir}/mypy-requirements.txt - api: pip install -r {toxinidir}/opentelemetry-api/test-requirements.txt + py3{8,9}-test-opentelemetry-api: pip install -r {toxinidir}/opentelemetry-api/test-requirements-0.txt + pypy3-test-opentelemetry-api: pip install -r {toxinidir}/opentelemetry-api/test-requirements-0.txt + py3{10,11,12}-test-opentelemetry-api: pip install -r {toxinidir}/opentelemetry-api/test-requirements-1.txt + lint-opentelemetry-api: pip install -r {toxinidir}/opentelemetry-api/test-requirements-1.txt sdk: pip install -r {toxinidir}/opentelemetry-sdk/test-requirements.txt benchmark-opentelemetry-sdk: pip install -r {toxinidir}/opentelemetry-sdk/benchmark-requirements.txt