diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index b9be3d8936..3f9c541ff4 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -28,6 +28,11 @@ # Python 3.10 and below BaseExceptionGroup = None # type: ignore +try: + from importlib import metadata as importlib_metadata +except ImportError: + importlib_metadata = None # type: ignore[assignment] + from typing import TYPE_CHECKING import sentry_sdk @@ -1841,6 +1846,14 @@ def package_version(package: str) -> "Optional[Tuple[int, ...]]": _replace_hyphens_dots_and_underscores_with_dashes(package) ) + if importlib_metadata is not None: + # Look up the single distribution instead of scanning (and parsing + # the metadata of) every installed package on each call. + try: + return parse_version(importlib_metadata.version(normalized_package)) + except importlib_metadata.PackageNotFoundError: + return None + installed_packages = { _replace_hyphens_dots_and_underscores_with_dashes(module): v for module, v in _get_installed_modules().items() diff --git a/tests/test_utils.py b/tests/test_utils.py index 77def0eb6e..0bea9a6705 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1412,6 +1412,24 @@ def test_package_version_is_none(): assert package_version("non_existent_package") is None +@pytest.mark.skipif(sys.version_info < (3, 8), reason="requires importlib.metadata") +def test_package_version_installed_package(): + from importlib import metadata + + assert package_version("pytest") == parse_version(metadata.version("pytest")) + + +@pytest.mark.skipif(sys.version_info < (3, 8), reason="requires importlib.metadata") +def test_package_version_does_not_scan_all_installed_packages(monkeypatch): + def _fail(): + raise AssertionError("package_version must not scan all installed packages") + + monkeypatch.setattr("sentry_sdk.utils._get_installed_modules", _fail) + + assert package_version("pytest") is not None + assert package_version("non_existent_package") is None + + @pytest.mark.parametrize( "kwargs, expected", [