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 all 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
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 @@
@staticmethod
def setup_once():
# type: () -> None
installed_packages = _get_installed_modules()
version = parse_version(installed_packages["ariadne"])
version = package_version("ariadne")

Check warning on line 35 in sentry_sdk/integrations/ariadne.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/integrations/ariadne.py#L35

Added line #L35 was not covered by tests

if version is None:
raise DidNotEnable("Unparsable ariadne version: {}".format(version))
raise DidNotEnable("Unparsable ariadne version.")

Check warning on line 38 in sentry_sdk/integrations/ariadne.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/integrations/ariadne.py#L38

Added line #L38 was not covered by tests

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 @@
@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.")

Check warning on line 69 in sentry_sdk/integrations/flask.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/integrations/flask.py#L69

Added line #L69 was not covered by tests

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 @@
@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.")

Check warning on line 33 in sentry_sdk/integrations/graphene.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/integrations/graphene.py#L33

Added line #L33 was not covered by tests

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 @@
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 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

Check warning on line 1571 in sentry_sdk/utils.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/utils.py#L1570-L1571

Added lines #L1570 - L1571 were not covered by tests

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

Check warning on line 1597 in sentry_sdk/utils.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/utils.py#L1596-L1597

Added lines #L1596 - L1597 were not covered by tests

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

@sentrivana sentrivana Nov 16, 2023

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

Check warning on line 1621 in sentry_sdk/utils.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/utils.py#L1621

Added line #L1621 was not covered by tests

return parse_version(version)


if PY37:

def nanosecond_time():
Expand Down
Loading
Loading