Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move installed modules code to utils #2429

Merged
merged 21 commits into from
Nov 24, 2023
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
df24e1a
Move installed modules code to utils
sentrivana Oct 10, 2023
1c45a0b
Merge branch 'master' into ivana/package-version-helper
sentrivana Oct 10, 2023
971ec26
add strawberry
sentrivana Oct 10, 2023
95c2fa9
Merge branch 'master' into ivana/package-version-helper
antonpirker Oct 11, 2023
539b6bd
Merge branch 'master' into ivana/package-version-helper
sentrivana Oct 11, 2023
26f20ba
Merge branch 'master' into ivana/package-version-helper
sentrivana Oct 12, 2023
d9c0e8f
Merge branch 'master' into ivana/package-version-helper
sentrivana Oct 13, 2023
b6c7a51
Merge branch 'master' into ivana/package-version-helper
sentrivana Oct 16, 2023
96bd4d8
add a test
sentrivana Oct 16, 2023
d50568b
Merge branch 'master' into ivana/package-version-helper
sentrivana Oct 16, 2023
d24bbd2
compat
sentrivana Oct 16, 2023
5362ade
Merge branch 'master' into ivana/package-version-helper
sentrivana Oct 17, 2023
b10344c
Merge branch 'master' into ivana/package-version-helper
sentrivana Oct 17, 2023
e76aa5d
Merge branch 'master' into ivana/package-version-helper
sentrivana Oct 24, 2023
f016218
Merge branch 'master' into ivana/package-version-helper
sentrivana Oct 25, 2023
0032f64
Merge branch 'master' into ivana/package-version-helper
sentrivana Nov 13, 2023
bf2e65a
Merge branch 'master' into ivana/package-version-helper
sentrivana Nov 16, 2023
85169b2
Merge branch 'master' into ivana/package-version-helper
sentrivana Nov 23, 2023
6782fd3
Merge branch 'master' into ivana/package-version-helper
sentrivana Nov 24, 2023
c9b6de5
Merge branch 'master' into ivana/package-version-helper
antonpirker Nov 24, 2023
4ca4e0d
Merge branch 'master' into ivana/package-version-helper
antonpirker Nov 24, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 3 additions & 5 deletions sentry_sdk/integrations/ariadne.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.")
Expand Down
2 changes: 1 addition & 1 deletion sentry_sdk/integrations/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -34,6 +33,7 @@
CONTEXTVARS_ERROR_MESSAGE,
logger,
transaction_from_function,
_get_installed_modules,
)
from sentry_sdk.tracing import Transaction

Expand Down
10 changes: 3 additions & 7 deletions sentry_sdk/integrations/flask.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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.")
Expand Down
8 changes: 3 additions & 5 deletions sentry_sdk/integrations/graphene.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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.")
Expand Down
46 changes: 1 addition & 45 deletions sentry_sdk/integrations/modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
3 changes: 1 addition & 2 deletions sentry_sdk/integrations/opentelemetry/integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
7 changes: 3 additions & 4 deletions sentry_sdk/integrations/strawberry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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(
Expand Down
51 changes: 51 additions & 0 deletions sentry_sdk/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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/+=]*$")

Expand Down Expand Up @@ -1572,6 +1573,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
Comment on lines +1608 to +1613
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it perhaps be a good idea to place this code, along with the _installed_modules into a static class, so we can avoid having a global variable? I guess the static class would still effectively be storing a global state, so perhaps it makes only a small difference here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could wrap this in a static class but then it'd be a bit inconsistent to have some installed modules related code in the class (the _get_installed_modules function above + the _installed_modules var) and some of it outside (e.g. the package_version helper). We could put everything in the class, but it's not great API to have to then import the class to use a small utility function. Also, you technically are introducing a global variable in any case, it's just about whether turning the whole thing into a class brings any additional benefits. I'd prefer to keep this as is if that's ok with you.



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():
Expand Down
59 changes: 1 addition & 58 deletions tests/integrations/modules/test_modules.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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")