Skip to content

Commit

Permalink
Move installed modules code to utils (#2429)
Browse files Browse the repository at this point in the history
Even though we're now using the `_get_installed_modules` function in many different places, it still lives in `sentry_sdk.integrations.modules`. With this change we move `_get_installed_modules` (and related helpers) to `utils.py` and introduce a new `package_version` helper function (also in `utils.py`) that finds out and parses the version of a package in one go.
  • Loading branch information
sentrivana committed Nov 24, 2023
1 parent f6325f7 commit 5ee3c18
Show file tree
Hide file tree
Showing 10 changed files with 188 additions and 179 deletions.
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
155 changes: 103 additions & 52 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 @@ -1126,58 +1127,6 @@ def strip_string(value, max_length=None):
return value


def parse_version(version):
# type: (str) -> Optional[Tuple[int, ...]]
"""
Parses a version string into a tuple of integers.
This uses the parsing loging from PEP 440:
https://peps.python.org/pep-0440/#appendix-b-parsing-version-strings-with-regular-expressions
"""
VERSION_PATTERN = r""" # noqa: N806
v?
(?:
(?:(?P<epoch>[0-9]+)!)? # epoch
(?P<release>[0-9]+(?:\.[0-9]+)*) # release segment
(?P<pre> # pre-release
[-_\.]?
(?P<pre_l>(a|b|c|rc|alpha|beta|pre|preview))
[-_\.]?
(?P<pre_n>[0-9]+)?
)?
(?P<post> # post release
(?:-(?P<post_n1>[0-9]+))
|
(?:
[-_\.]?
(?P<post_l>post|rev|r)
[-_\.]?
(?P<post_n2>[0-9]+)?
)
)?
(?P<dev> # dev release
[-_\.]?
(?P<dev_l>dev)
[-_\.]?
(?P<dev_n>[0-9]+)?
)?
)
(?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))? # local version
"""

pattern = re.compile(
r"^\s*" + VERSION_PATTERN + r"\s*$",
re.VERBOSE | re.IGNORECASE,
)

try:
release = pattern.match(version).groupdict()["release"] # type: ignore
release_tuple = tuple(map(int, release.split(".")[:3])) # type: Tuple[int, ...]
except (TypeError, ValueError, AttributeError):
return None

return release_tuple


def _is_contextvars_broken():
# type: () -> bool
"""
Expand Down Expand Up @@ -1572,6 +1521,108 @@ def is_sentry_url(hub, url):
)


def parse_version(version):
# type: (str) -> Optional[Tuple[int, ...]]
"""
Parses a version string into a tuple of integers.
This uses the parsing loging from PEP 440:
https://peps.python.org/pep-0440/#appendix-b-parsing-version-strings-with-regular-expressions
"""
VERSION_PATTERN = r""" # noqa: N806
v?
(?:
(?:(?P<epoch>[0-9]+)!)? # epoch
(?P<release>[0-9]+(?:\.[0-9]+)*) # release segment
(?P<pre> # pre-release
[-_\.]?
(?P<pre_l>(a|b|c|rc|alpha|beta|pre|preview))
[-_\.]?
(?P<pre_n>[0-9]+)?
)?
(?P<post> # post release
(?:-(?P<post_n1>[0-9]+))
|
(?:
[-_\.]?
(?P<post_l>post|rev|r)
[-_\.]?
(?P<post_n2>[0-9]+)?
)
)?
(?P<dev> # dev release
[-_\.]?
(?P<dev_l>dev)
[-_\.]?
(?P<dev_n>[0-9]+)?
)?
)
(?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))? # local version
"""

pattern = re.compile(
r"^\s*" + VERSION_PATTERN + r"\s*$",
re.VERBOSE | re.IGNORECASE,
)

try:
release = pattern.match(version).groupdict()["release"] # type: ignore
release_tuple = tuple(map(int, release.split(".")[:3])) # type: Tuple[int, ...]
except (TypeError, ValueError, AttributeError):
return None

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():
Expand Down

0 comments on commit 5ee3c18

Please sign in to comment.