Skip to content

Commit 5ee3c18

Browse files
authored
Move installed modules code to utils (#2429)
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.
1 parent f6325f7 commit 5ee3c18

File tree

10 files changed

+188
-179
lines changed

10 files changed

+188
-179
lines changed

sentry_sdk/integrations/ariadne.py

+3-5
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,11 @@
33
from sentry_sdk.hub import Hub, _should_send_default_pii
44
from sentry_sdk.integrations import DidNotEnable, Integration
55
from sentry_sdk.integrations.logging import ignore_logger
6-
from sentry_sdk.integrations.modules import _get_installed_modules
76
from sentry_sdk.integrations._wsgi_common import request_body_within_bounds
87
from sentry_sdk.utils import (
98
capture_internal_exceptions,
109
event_from_exception,
11-
parse_version,
10+
package_version,
1211
)
1312
from sentry_sdk._types import TYPE_CHECKING
1413

@@ -33,11 +32,10 @@ class AriadneIntegration(Integration):
3332
@staticmethod
3433
def setup_once():
3534
# type: () -> None
36-
installed_packages = _get_installed_modules()
37-
version = parse_version(installed_packages["ariadne"])
35+
version = package_version("ariadne")
3836

3937
if version is None:
40-
raise DidNotEnable("Unparsable ariadne version: {}".format(version))
38+
raise DidNotEnable("Unparsable ariadne version.")
4139

4240
if version < (0, 20):
4341
raise DidNotEnable("ariadne 0.20 or newer required.")

sentry_sdk/integrations/asgi.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
_get_request_data,
2020
_get_url,
2121
)
22-
from sentry_sdk.integrations.modules import _get_installed_modules
2322
from sentry_sdk.sessions import auto_session_tracking
2423
from sentry_sdk.tracing import (
2524
SOURCE_FOR_STYLE,
@@ -34,6 +33,7 @@
3433
CONTEXTVARS_ERROR_MESSAGE,
3534
logger,
3635
transaction_from_function,
36+
_get_installed_modules,
3737
)
3838
from sentry_sdk.tracing import Transaction
3939

sentry_sdk/integrations/flask.py

+3-7
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,12 @@
55
from sentry_sdk.integrations import DidNotEnable, Integration
66
from sentry_sdk.integrations._wsgi_common import RequestExtractor
77
from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware
8-
from sentry_sdk.integrations.modules import _get_installed_modules
98
from sentry_sdk.scope import Scope
109
from sentry_sdk.tracing import SOURCE_FOR_STYLE
1110
from sentry_sdk.utils import (
1211
capture_internal_exceptions,
1312
event_from_exception,
14-
parse_version,
13+
package_version,
1514
)
1615

1716
if TYPE_CHECKING:
@@ -64,13 +63,10 @@ def __init__(self, transaction_style="endpoint"):
6463
@staticmethod
6564
def setup_once():
6665
# type: () -> None
67-
68-
installed_packages = _get_installed_modules()
69-
flask_version = installed_packages["flask"]
70-
version = parse_version(flask_version)
66+
version = package_version("flask")
7167

7268
if version is None:
73-
raise DidNotEnable("Unparsable Flask version: {}".format(flask_version))
69+
raise DidNotEnable("Unparsable Flask version.")
7470

7571
if version < (0, 10):
7672
raise DidNotEnable("Flask 0.10 or newer is required.")

sentry_sdk/integrations/graphene.py

+3-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
from sentry_sdk.hub import Hub, _should_send_default_pii
22
from sentry_sdk.integrations import DidNotEnable, Integration
3-
from sentry_sdk.integrations.modules import _get_installed_modules
43
from sentry_sdk.utils import (
54
capture_internal_exceptions,
65
event_from_exception,
7-
parse_version,
6+
package_version,
87
)
98
from sentry_sdk._types import TYPE_CHECKING
109

@@ -28,11 +27,10 @@ class GrapheneIntegration(Integration):
2827
@staticmethod
2928
def setup_once():
3029
# type: () -> None
31-
installed_packages = _get_installed_modules()
32-
version = parse_version(installed_packages["graphene"])
30+
version = package_version("graphene")
3331

3432
if version is None:
35-
raise DidNotEnable("Unparsable graphene version: {}".format(version))
33+
raise DidNotEnable("Unparsable graphene version.")
3634

3735
if version < (3, 3):
3836
raise DidNotEnable("graphene 3.3 or newer required.")

sentry_sdk/integrations/modules.py

+1-45
Original file line numberDiff line numberDiff line change
@@ -3,61 +3,17 @@
33
from sentry_sdk.hub import Hub
44
from sentry_sdk.integrations import Integration
55
from sentry_sdk.scope import add_global_event_processor
6+
from sentry_sdk.utils import _get_installed_modules
67

78
from sentry_sdk._types import TYPE_CHECKING
89

910
if TYPE_CHECKING:
1011
from typing import Any
1112
from typing import Dict
12-
from typing import Tuple
13-
from typing import Iterator
1413

1514
from sentry_sdk._types import Event
1615

1716

18-
_installed_modules = None
19-
20-
21-
def _normalize_module_name(name):
22-
# type: (str) -> str
23-
return name.lower()
24-
25-
26-
def _generate_installed_modules():
27-
# type: () -> Iterator[Tuple[str, str]]
28-
try:
29-
from importlib import metadata
30-
31-
for dist in metadata.distributions():
32-
name = dist.metadata["Name"]
33-
# `metadata` values may be `None`, see:
34-
# https://github.com/python/cpython/issues/91216
35-
# and
36-
# https://github.com/python/importlib_metadata/issues/371
37-
if name is not None:
38-
version = metadata.version(name)
39-
if version is not None:
40-
yield _normalize_module_name(name), version
41-
42-
except ImportError:
43-
# < py3.8
44-
try:
45-
import pkg_resources
46-
except ImportError:
47-
return
48-
49-
for info in pkg_resources.working_set:
50-
yield _normalize_module_name(info.key), info.version
51-
52-
53-
def _get_installed_modules():
54-
# type: () -> Dict[str, str]
55-
global _installed_modules
56-
if _installed_modules is None:
57-
_installed_modules = dict(_generate_installed_modules())
58-
return _installed_modules
59-
60-
6117
class ModulesIntegration(Integration):
6218
identifier = "modules"
6319

sentry_sdk/integrations/opentelemetry/integration.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@
99
from sentry_sdk.integrations import DidNotEnable, Integration
1010
from sentry_sdk.integrations.opentelemetry.span_processor import SentrySpanProcessor
1111
from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator
12-
from sentry_sdk.integrations.modules import _get_installed_modules
13-
from sentry_sdk.utils import logger
12+
from sentry_sdk.utils import logger, _get_installed_modules
1413
from sentry_sdk._types import TYPE_CHECKING
1514

1615
try:

sentry_sdk/integrations/strawberry.py

+3-4
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@
55
from sentry_sdk.consts import OP
66
from sentry_sdk.integrations import Integration, DidNotEnable
77
from sentry_sdk.integrations.logging import ignore_logger
8-
from sentry_sdk.integrations.modules import _get_installed_modules
98
from sentry_sdk.hub import Hub, _should_send_default_pii
109
from sentry_sdk.utils import (
1110
capture_internal_exceptions,
1211
event_from_exception,
1312
logger,
14-
parse_version,
13+
package_version,
14+
_get_installed_modules,
1515
)
1616
from sentry_sdk._types import TYPE_CHECKING
1717

@@ -55,8 +55,7 @@ def __init__(self, async_execution=None):
5555
@staticmethod
5656
def setup_once():
5757
# type: () -> None
58-
installed_packages = _get_installed_modules()
59-
version = parse_version(installed_packages["strawberry-graphql"])
58+
version = package_version("strawberry-graphql")
6059

6160
if version is None:
6261
raise DidNotEnable(

sentry_sdk/utils.py

+103-52
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
# The logger is created here but initialized in the debug support module
7777
logger = logging.getLogger("sentry_sdk.errors")
7878

79+
_installed_modules = None
7980

8081
BASE64_ALPHABET = re.compile(r"^[a-zA-Z0-9/+=]*$")
8182

@@ -1126,58 +1127,6 @@ def strip_string(value, max_length=None):
11261127
return value
11271128

11281129

1129-
def parse_version(version):
1130-
# type: (str) -> Optional[Tuple[int, ...]]
1131-
"""
1132-
Parses a version string into a tuple of integers.
1133-
This uses the parsing loging from PEP 440:
1134-
https://peps.python.org/pep-0440/#appendix-b-parsing-version-strings-with-regular-expressions
1135-
"""
1136-
VERSION_PATTERN = r""" # noqa: N806
1137-
v?
1138-
(?:
1139-
(?:(?P<epoch>[0-9]+)!)? # epoch
1140-
(?P<release>[0-9]+(?:\.[0-9]+)*) # release segment
1141-
(?P<pre> # pre-release
1142-
[-_\.]?
1143-
(?P<pre_l>(a|b|c|rc|alpha|beta|pre|preview))
1144-
[-_\.]?
1145-
(?P<pre_n>[0-9]+)?
1146-
)?
1147-
(?P<post> # post release
1148-
(?:-(?P<post_n1>[0-9]+))
1149-
|
1150-
(?:
1151-
[-_\.]?
1152-
(?P<post_l>post|rev|r)
1153-
[-_\.]?
1154-
(?P<post_n2>[0-9]+)?
1155-
)
1156-
)?
1157-
(?P<dev> # dev release
1158-
[-_\.]?
1159-
(?P<dev_l>dev)
1160-
[-_\.]?
1161-
(?P<dev_n>[0-9]+)?
1162-
)?
1163-
)
1164-
(?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))? # local version
1165-
"""
1166-
1167-
pattern = re.compile(
1168-
r"^\s*" + VERSION_PATTERN + r"\s*$",
1169-
re.VERBOSE | re.IGNORECASE,
1170-
)
1171-
1172-
try:
1173-
release = pattern.match(version).groupdict()["release"] # type: ignore
1174-
release_tuple = tuple(map(int, release.split(".")[:3])) # type: Tuple[int, ...]
1175-
except (TypeError, ValueError, AttributeError):
1176-
return None
1177-
1178-
return release_tuple
1179-
1180-
11811130
def _is_contextvars_broken():
11821131
# type: () -> bool
11831132
"""
@@ -1572,6 +1521,108 @@ def is_sentry_url(hub, url):
15721521
)
15731522

15741523

1524+
def parse_version(version):
1525+
# type: (str) -> Optional[Tuple[int, ...]]
1526+
"""
1527+
Parses a version string into a tuple of integers.
1528+
This uses the parsing loging from PEP 440:
1529+
https://peps.python.org/pep-0440/#appendix-b-parsing-version-strings-with-regular-expressions
1530+
"""
1531+
VERSION_PATTERN = r""" # noqa: N806
1532+
v?
1533+
(?:
1534+
(?:(?P<epoch>[0-9]+)!)? # epoch
1535+
(?P<release>[0-9]+(?:\.[0-9]+)*) # release segment
1536+
(?P<pre> # pre-release
1537+
[-_\.]?
1538+
(?P<pre_l>(a|b|c|rc|alpha|beta|pre|preview))
1539+
[-_\.]?
1540+
(?P<pre_n>[0-9]+)?
1541+
)?
1542+
(?P<post> # post release
1543+
(?:-(?P<post_n1>[0-9]+))
1544+
|
1545+
(?:
1546+
[-_\.]?
1547+
(?P<post_l>post|rev|r)
1548+
[-_\.]?
1549+
(?P<post_n2>[0-9]+)?
1550+
)
1551+
)?
1552+
(?P<dev> # dev release
1553+
[-_\.]?
1554+
(?P<dev_l>dev)
1555+
[-_\.]?
1556+
(?P<dev_n>[0-9]+)?
1557+
)?
1558+
)
1559+
(?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))? # local version
1560+
"""
1561+
1562+
pattern = re.compile(
1563+
r"^\s*" + VERSION_PATTERN + r"\s*$",
1564+
re.VERBOSE | re.IGNORECASE,
1565+
)
1566+
1567+
try:
1568+
release = pattern.match(version).groupdict()["release"] # type: ignore
1569+
release_tuple = tuple(map(int, release.split(".")[:3])) # type: Tuple[int, ...]
1570+
except (TypeError, ValueError, AttributeError):
1571+
return None
1572+
1573+
return release_tuple
1574+
1575+
1576+
def _generate_installed_modules():
1577+
# type: () -> Iterator[Tuple[str, str]]
1578+
try:
1579+
from importlib import metadata
1580+
1581+
for dist in metadata.distributions():
1582+
name = dist.metadata["Name"]
1583+
# `metadata` values may be `None`, see:
1584+
# https://github.com/python/cpython/issues/91216
1585+
# and
1586+
# https://github.com/python/importlib_metadata/issues/371
1587+
if name is not None:
1588+
version = metadata.version(name)
1589+
if version is not None:
1590+
yield _normalize_module_name(name), version
1591+
1592+
except ImportError:
1593+
# < py3.8
1594+
try:
1595+
import pkg_resources
1596+
except ImportError:
1597+
return
1598+
1599+
for info in pkg_resources.working_set:
1600+
yield _normalize_module_name(info.key), info.version
1601+
1602+
1603+
def _normalize_module_name(name):
1604+
# type: (str) -> str
1605+
return name.lower()
1606+
1607+
1608+
def _get_installed_modules():
1609+
# type: () -> Dict[str, str]
1610+
global _installed_modules
1611+
if _installed_modules is None:
1612+
_installed_modules = dict(_generate_installed_modules())
1613+
return _installed_modules
1614+
1615+
1616+
def package_version(package):
1617+
# type: (str) -> Optional[Tuple[int, ...]]
1618+
installed_packages = _get_installed_modules()
1619+
version = installed_packages.get(package)
1620+
if version is None:
1621+
return None
1622+
1623+
return parse_version(version)
1624+
1625+
15751626
if PY37:
15761627

15771628
def nanosecond_time():

0 commit comments

Comments
 (0)