From df24e1a25990bf30b5d09d130b523ce07a29584e Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Tue, 10 Oct 2023 14:10:47 +0200 Subject: [PATCH 1/4] Move installed modules code to utils --- sentry_sdk/integrations/ariadne.py | 8 +-- sentry_sdk/integrations/asgi.py | 2 +- sentry_sdk/integrations/flask.py | 10 +--- sentry_sdk/integrations/graphene.py | 8 +-- sentry_sdk/integrations/modules.py | 46 +-------------- .../integrations/opentelemetry/integration.py | 3 +- sentry_sdk/utils.py | 51 ++++++++++++++++ tests/integrations/modules/test_modules.py | 59 +------------------ tests/test_utils.py | 53 +++++++++++++++++ 9 files changed, 117 insertions(+), 123 deletions(-) diff --git a/sentry_sdk/integrations/ariadne.py b/sentry_sdk/integrations/ariadne.py index 8025860a6f..86d6b5e28e 100644 --- a/sentry_sdk/integrations/ariadne.py +++ b/sentry_sdk/integrations/ariadne.py @@ -3,12 +3,11 @@ from sentry_sdk.hub import Hub, _should_send_default_pii from sentry_sdk.integrations import DidNotEnable, Integration from sentry_sdk.integrations.logging import ignore_logger -from sentry_sdk.integrations.modules import _get_installed_modules from sentry_sdk.integrations._wsgi_common import request_body_within_bounds from sentry_sdk.utils import ( capture_internal_exceptions, event_from_exception, - parse_version, + package_version, ) from sentry_sdk._types import TYPE_CHECKING @@ -33,11 +32,10 @@ class AriadneIntegration(Integration): @staticmethod def setup_once(): # type: () -> None - installed_packages = _get_installed_modules() - version = parse_version(installed_packages["ariadne"]) + version = package_version("ariadne") if version is None: - raise DidNotEnable("Unparsable ariadne version: {}".format(version)) + raise DidNotEnable("Unparsable ariadne version.") if version < (0, 20): raise DidNotEnable("ariadne 0.20 or newer required.") diff --git a/sentry_sdk/integrations/asgi.py b/sentry_sdk/integrations/asgi.py index 2cecdf9a81..901c6f5d23 100644 --- a/sentry_sdk/integrations/asgi.py +++ b/sentry_sdk/integrations/asgi.py @@ -19,7 +19,6 @@ _get_request_data, _get_url, ) -from sentry_sdk.integrations.modules import _get_installed_modules from sentry_sdk.sessions import auto_session_tracking from sentry_sdk.tracing import ( SOURCE_FOR_STYLE, @@ -34,6 +33,7 @@ CONTEXTVARS_ERROR_MESSAGE, logger, transaction_from_function, + _get_installed_modules, ) from sentry_sdk.tracing import Transaction diff --git a/sentry_sdk/integrations/flask.py b/sentry_sdk/integrations/flask.py index 0da411c23d..453ab48ce3 100644 --- a/sentry_sdk/integrations/flask.py +++ b/sentry_sdk/integrations/flask.py @@ -5,13 +5,12 @@ from sentry_sdk.integrations import DidNotEnable, Integration from sentry_sdk.integrations._wsgi_common import RequestExtractor from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware -from sentry_sdk.integrations.modules import _get_installed_modules from sentry_sdk.scope import Scope from sentry_sdk.tracing import SOURCE_FOR_STYLE from sentry_sdk.utils import ( capture_internal_exceptions, event_from_exception, - parse_version, + package_version, ) if TYPE_CHECKING: @@ -64,13 +63,10 @@ def __init__(self, transaction_style="endpoint"): @staticmethod def setup_once(): # type: () -> None - - installed_packages = _get_installed_modules() - flask_version = installed_packages["flask"] - version = parse_version(flask_version) + version = package_version("flask") if version is None: - raise DidNotEnable("Unparsable Flask version: {}".format(flask_version)) + raise DidNotEnable("Unparsable Flask version.") if version < (0, 10): raise DidNotEnable("Flask 0.10 or newer is required.") diff --git a/sentry_sdk/integrations/graphene.py b/sentry_sdk/integrations/graphene.py index 5d3c656145..fa753d0812 100644 --- a/sentry_sdk/integrations/graphene.py +++ b/sentry_sdk/integrations/graphene.py @@ -1,10 +1,9 @@ from sentry_sdk.hub import Hub, _should_send_default_pii from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.integrations.modules import _get_installed_modules from sentry_sdk.utils import ( capture_internal_exceptions, event_from_exception, - parse_version, + package_version, ) from sentry_sdk._types import TYPE_CHECKING @@ -28,11 +27,10 @@ class GrapheneIntegration(Integration): @staticmethod def setup_once(): # type: () -> None - installed_packages = _get_installed_modules() - version = parse_version(installed_packages["graphene"]) + version = package_version("graphene") if version is None: - raise DidNotEnable("Unparsable graphene version: {}".format(version)) + raise DidNotEnable("Unparsable graphene version.") if version < (3, 3): raise DidNotEnable("graphene 3.3 or newer required.") diff --git a/sentry_sdk/integrations/modules.py b/sentry_sdk/integrations/modules.py index 3f9f356eed..5b595b4032 100644 --- a/sentry_sdk/integrations/modules.py +++ b/sentry_sdk/integrations/modules.py @@ -3,61 +3,17 @@ from sentry_sdk.hub import Hub from sentry_sdk.integrations import Integration from sentry_sdk.scope import add_global_event_processor +from sentry_sdk.utils import _get_installed_modules from sentry_sdk._types import TYPE_CHECKING if TYPE_CHECKING: from typing import Any from typing import Dict - from typing import Tuple - from typing import Iterator from sentry_sdk._types import Event -_installed_modules = None - - -def _normalize_module_name(name): - # type: (str) -> str - return name.lower() - - -def _generate_installed_modules(): - # type: () -> Iterator[Tuple[str, str]] - try: - from importlib import metadata - - for dist in metadata.distributions(): - name = dist.metadata["Name"] - # `metadata` values may be `None`, see: - # https://github.com/python/cpython/issues/91216 - # and - # https://github.com/python/importlib_metadata/issues/371 - if name is not None: - version = metadata.version(name) - if version is not None: - yield _normalize_module_name(name), version - - except ImportError: - # < py3.8 - try: - import pkg_resources - except ImportError: - return - - for info in pkg_resources.working_set: - yield _normalize_module_name(info.key), info.version - - -def _get_installed_modules(): - # type: () -> Dict[str, str] - global _installed_modules - if _installed_modules is None: - _installed_modules = dict(_generate_installed_modules()) - return _installed_modules - - class ModulesIntegration(Integration): identifier = "modules" diff --git a/sentry_sdk/integrations/opentelemetry/integration.py b/sentry_sdk/integrations/opentelemetry/integration.py index 20dc4625df..e1a4318f67 100644 --- a/sentry_sdk/integrations/opentelemetry/integration.py +++ b/sentry_sdk/integrations/opentelemetry/integration.py @@ -9,8 +9,7 @@ from sentry_sdk.integrations import DidNotEnable, Integration from sentry_sdk.integrations.opentelemetry.span_processor import SentrySpanProcessor from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator -from sentry_sdk.integrations.modules import _get_installed_modules -from sentry_sdk.utils import logger +from sentry_sdk.utils import logger, _get_installed_modules from sentry_sdk._types import TYPE_CHECKING try: diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index c811d2d2fe..f9fb4ebfe3 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -76,6 +76,7 @@ # The logger is created here but initialized in the debug support module logger = logging.getLogger("sentry_sdk.errors") +_installed_modules = None BASE64_ALPHABET = re.compile(r"^[a-zA-Z0-9/+=]*$") @@ -1563,6 +1564,56 @@ def parse_version(version): return release_tuple +def _generate_installed_modules(): + # type: () -> Iterator[Tuple[str, str]] + try: + from importlib import metadata + + for dist in metadata.distributions(): + name = dist.metadata["Name"] + # `metadata` values may be `None`, see: + # https://github.com/python/cpython/issues/91216 + # and + # https://github.com/python/importlib_metadata/issues/371 + if name is not None: + version = metadata.version(name) + if version is not None: + yield _normalize_module_name(name), version + + except ImportError: + # < py3.8 + try: + import pkg_resources + except ImportError: + return + + for info in pkg_resources.working_set: + yield _normalize_module_name(info.key), info.version + + +def _normalize_module_name(name): + # type: (str) -> str + return name.lower() + + +def _get_installed_modules(): + # type: () -> Dict[str, str] + global _installed_modules + if _installed_modules is None: + _installed_modules = dict(_generate_installed_modules()) + return _installed_modules + + +def package_version(package): + # type: (str) -> Optional[Tuple[int, ...]] + installed_packages = _get_installed_modules() + version = installed_packages.get(package) + if version is None: + return None + + return parse_version(version) + + if PY37: def nanosecond_time(): diff --git a/tests/integrations/modules/test_modules.py b/tests/integrations/modules/test_modules.py index c7097972b0..3f4d7bd9dc 100644 --- a/tests/integrations/modules/test_modules.py +++ b/tests/integrations/modules/test_modules.py @@ -1,22 +1,6 @@ -import pytest -import re import sentry_sdk -from sentry_sdk.integrations.modules import ( - ModulesIntegration, - _get_installed_modules, -) - - -def _normalize_distribution_name(name): - # type: (str) -> str - """Normalize distribution name according to PEP-0503. - - See: - https://peps.python.org/pep-0503/#normalized-names - for more details. - """ - return re.sub(r"[-_.]+", "-", name).lower() +from sentry_sdk.integrations.modules import ModulesIntegration def test_basic(sentry_init, capture_events): @@ -28,44 +12,3 @@ def test_basic(sentry_init, capture_events): (event,) = events assert "sentry-sdk" in event["modules"] assert "pytest" in event["modules"] - - -def test_installed_modules(): - try: - from importlib.metadata import distributions, version - - importlib_available = True - except ImportError: - importlib_available = False - - try: - import pkg_resources - - pkg_resources_available = True - except ImportError: - pkg_resources_available = False - - installed_distributions = { - _normalize_distribution_name(dist): version - for dist, version in _get_installed_modules().items() - } - - if importlib_available: - importlib_distributions = { - _normalize_distribution_name(dist.metadata["Name"]): version( - dist.metadata["Name"] - ) - for dist in distributions() - if dist.metadata["Name"] is not None - and version(dist.metadata["Name"]) is not None - } - assert installed_distributions == importlib_distributions - - elif pkg_resources_available: - pkg_resources_distributions = { - _normalize_distribution_name(dist.key): dist.version - for dist in pkg_resources.working_set - } - assert installed_distributions == pkg_resources_distributions - else: - pytest.fail("Neither importlib nor pkg_resources is available") diff --git a/tests/test_utils.py b/tests/test_utils.py index ee73433dd5..66ff59f650 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -15,6 +15,7 @@ sanitize_url, serialize_frame, is_sentry_url, + _get_installed_modules, ) import sentry_sdk @@ -25,6 +26,17 @@ import mock # python < 3.3 +def _normalize_distribution_name(name): + # type: (str) -> str + """Normalize distribution name according to PEP-0503. + + See: + https://peps.python.org/pep-0503/#normalized-names + for more details. + """ + return re.sub(r"[-_.]+", "-", name).lower() + + @pytest.mark.parametrize( ("url", "expected_result"), [ @@ -488,3 +500,44 @@ def test_get_error_message(error, expected_result): exc_value.detail = error raise Exception assert get_error_message(exc_value) == expected_result(exc_value) + + +def test_installed_modules(): + try: + from importlib.metadata import distributions, version + + importlib_available = True + except ImportError: + importlib_available = False + + try: + import pkg_resources + + pkg_resources_available = True + except ImportError: + pkg_resources_available = False + + installed_distributions = { + _normalize_distribution_name(dist): version + for dist, version in _get_installed_modules().items() + } + + if importlib_available: + importlib_distributions = { + _normalize_distribution_name(dist.metadata["Name"]): version( + dist.metadata["Name"] + ) + for dist in distributions() + if dist.metadata["Name"] is not None + and version(dist.metadata["Name"]) is not None + } + assert installed_distributions == importlib_distributions + + elif pkg_resources_available: + pkg_resources_distributions = { + _normalize_distribution_name(dist.key): dist.version + for dist in pkg_resources.working_set + } + assert installed_distributions == pkg_resources_distributions + else: + pytest.fail("Neither importlib nor pkg_resources is available") From 971ec26881a6e9b96043cf29598c25865d35ca67 Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Tue, 10 Oct 2023 16:13:41 +0200 Subject: [PATCH 2/4] add strawberry --- sentry_sdk/integrations/strawberry.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/sentry_sdk/integrations/strawberry.py b/sentry_sdk/integrations/strawberry.py index 63ddc44f25..8f4314f663 100644 --- a/sentry_sdk/integrations/strawberry.py +++ b/sentry_sdk/integrations/strawberry.py @@ -5,13 +5,13 @@ from sentry_sdk.consts import OP from sentry_sdk.integrations import Integration, DidNotEnable from sentry_sdk.integrations.logging import ignore_logger -from sentry_sdk.integrations.modules import _get_installed_modules from sentry_sdk.hub import Hub, _should_send_default_pii from sentry_sdk.utils import ( capture_internal_exceptions, event_from_exception, logger, - parse_version, + package_version, + _get_installed_modules, ) from sentry_sdk._types import TYPE_CHECKING @@ -55,8 +55,7 @@ def __init__(self, async_execution=None): @staticmethod def setup_once(): # type: () -> None - installed_packages = _get_installed_modules() - version = parse_version(installed_packages["strawberry-graphql"]) + version = package_version("strawberry-graphql") if version is None: raise DidNotEnable( From 96bd4d8fdab2dbe984225de595ee56d0f83e6d0e Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Mon, 16 Oct 2023 11:25:16 +0200 Subject: [PATCH 3/4] add a test --- tests/test_utils.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/test_utils.py b/tests/test_utils.py index 66ff59f650..3d16009dff 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -541,3 +541,19 @@ def test_installed_modules(): assert installed_distributions == pkg_resources_distributions else: pytest.fail("Neither importlib nor pkg_resources is available") + + +def test_installed_modules_caching(): + mock_generate_installed_modules = mock.Mock() + mock_generate_installed_modules.return_value = {"package": "1.0.0"} + with mock.patch("sentry_sdk.utils._installed_modules", None): + with mock.patch( + "sentry_sdk.utils._generate_installed_modules", + mock_generate_installed_modules, + ): + _get_installed_modules() + mock_generate_installed_modules.assert_called_once() + mock_generate_installed_modules.reset_mock() + + _get_installed_modules() + mock_generate_installed_modules.assert_not_called() From d24bbd2cf0e5168cb465d2b04153ad166464e358 Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Mon, 16 Oct 2023 12:10:54 +0200 Subject: [PATCH 4/4] compat --- tests/test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index 3d16009dff..efbfa7504b 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -552,7 +552,7 @@ def test_installed_modules_caching(): mock_generate_installed_modules, ): _get_installed_modules() - mock_generate_installed_modules.assert_called_once() + assert mock_generate_installed_modules.called mock_generate_installed_modules.reset_mock() _get_installed_modules()