Skip to content

Commit

Permalink
Better version parsing in integrations (#2152)
Browse files Browse the repository at this point in the history
  • Loading branch information
antonpirker committed Jun 6, 2023
1 parent 692d099 commit 87eb761
Show file tree
Hide file tree
Showing 12 changed files with 147 additions and 45 deletions.
9 changes: 5 additions & 4 deletions sentry_sdk/integrations/aiohttp.py
Expand Up @@ -15,6 +15,7 @@
from sentry_sdk.utils import (
capture_internal_exceptions,
event_from_exception,
parse_version,
transaction_from_function,
HAS_REAL_CONTEXTVARS,
CONTEXTVARS_ERROR_MESSAGE,
Expand Down Expand Up @@ -64,10 +65,10 @@ def __init__(self, transaction_style="handler_name"):
def setup_once():
# type: () -> None

try:
version = tuple(map(int, AIOHTTP_VERSION.split(".")[:2]))
except (TypeError, ValueError):
raise DidNotEnable("AIOHTTP version unparsable: {}".format(AIOHTTP_VERSION))
version = parse_version(AIOHTTP_VERSION)

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

if version < (3, 4):
raise DidNotEnable("AIOHTTP 3.4 or newer required.")
Expand Down
9 changes: 7 additions & 2 deletions sentry_sdk/integrations/arq.py
Expand Up @@ -14,6 +14,7 @@
capture_internal_exceptions,
event_from_exception,
SENSITIVE_DATA_SUBSTITUTE,
parse_version,
)

try:
Expand Down Expand Up @@ -45,11 +46,15 @@ def setup_once():

try:
if isinstance(ARQ_VERSION, str):
version = tuple(map(int, ARQ_VERSION.split(".")[:2]))
version = parse_version(ARQ_VERSION)
else:
version = ARQ_VERSION.version[:2]

except (TypeError, ValueError):
raise DidNotEnable("arq version unparsable: {}".format(ARQ_VERSION))
version = None

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

if version < (0, 23):
raise DidNotEnable("arq 0.23 or newer required.")
Expand Down
11 changes: 7 additions & 4 deletions sentry_sdk/integrations/boto3.py
Expand Up @@ -7,7 +7,7 @@

from sentry_sdk._functools import partial
from sentry_sdk._types import TYPE_CHECKING
from sentry_sdk.utils import parse_url
from sentry_sdk.utils import parse_url, parse_version

if TYPE_CHECKING:
from typing import Any
Expand All @@ -30,14 +30,17 @@ class Boto3Integration(Integration):
@staticmethod
def setup_once():
# type: () -> None
try:
version = tuple(map(int, BOTOCORE_VERSION.split(".")[:3]))
except (ValueError, TypeError):

version = parse_version(BOTOCORE_VERSION)

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

if version < (1, 12):
raise DidNotEnable("Botocore 1.12 or newer is required.")

orig_init = BaseClient.__init__

def sentry_patched_init(self, *args, **kwargs):
Expand Down
9 changes: 5 additions & 4 deletions sentry_sdk/integrations/bottle.py
Expand Up @@ -5,6 +5,7 @@
from sentry_sdk.utils import (
capture_internal_exceptions,
event_from_exception,
parse_version,
transaction_from_function,
)
from sentry_sdk.integrations import Integration, DidNotEnable
Expand Down Expand Up @@ -57,10 +58,10 @@ def __init__(self, transaction_style="endpoint"):
def setup_once():
# type: () -> None

try:
version = tuple(map(int, BOTTLE_VERSION.replace("-dev", "").split(".")))
except (TypeError, ValueError):
raise DidNotEnable("Unparsable Bottle version: {}".format(version))
version = parse_version(BOTTLE_VERSION)

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

if version < (0, 12):
raise DidNotEnable("Bottle 0.12 or newer required.")
Expand Down
9 changes: 6 additions & 3 deletions sentry_sdk/integrations/chalice.py
Expand Up @@ -8,6 +8,7 @@
from sentry_sdk.utils import (
capture_internal_exceptions,
event_from_exception,
parse_version,
)
from sentry_sdk._types import TYPE_CHECKING
from sentry_sdk._functools import wraps
Expand Down Expand Up @@ -102,10 +103,12 @@ class ChaliceIntegration(Integration):
@staticmethod
def setup_once():
# type: () -> None
try:
version = tuple(map(int, CHALICE_VERSION.split(".")[:3]))
except (ValueError, TypeError):

version = parse_version(CHALICE_VERSION)

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

if version < (1, 20):
old_get_view_function_response = Chalice._get_view_function_response
else:
Expand Down
8 changes: 5 additions & 3 deletions sentry_sdk/integrations/falcon.py
Expand Up @@ -8,6 +8,7 @@
from sentry_sdk.utils import (
capture_internal_exceptions,
event_from_exception,
parse_version,
)

from sentry_sdk._types import TYPE_CHECKING
Expand Down Expand Up @@ -131,9 +132,10 @@ def __init__(self, transaction_style="uri_template"):
@staticmethod
def setup_once():
# type: () -> None
try:
version = tuple(map(int, FALCON_VERSION.split(".")))
except (ValueError, TypeError):

version = parse_version(FALCON_VERSION)

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

if version < (1, 4):
Expand Down
18 changes: 8 additions & 10 deletions sentry_sdk/integrations/flask.py
Expand Up @@ -10,6 +10,7 @@
from sentry_sdk.utils import (
capture_internal_exceptions,
event_from_exception,
parse_version,
)

if TYPE_CHECKING:
Expand Down Expand Up @@ -64,16 +65,13 @@ def __init__(self, transaction_style="endpoint"):
def setup_once():
# type: () -> None

# This version parsing is absolutely naive but the alternative is to
# import pkg_resources which slows down the SDK a lot.
try:
version = tuple(map(int, FLASK_VERSION.split(".")[:3]))
except (ValueError, TypeError):
# It's probably a release candidate, we assume it's fine.
pass
else:
if version < (0, 10):
raise DidNotEnable("Flask 0.10 or newer is required.")
version = parse_version(FLASK_VERSION)

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

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

before_render_template.connect(_add_sentry_trace)
request_started.connect(_request_started)
Expand Down
7 changes: 4 additions & 3 deletions sentry_sdk/integrations/rq.py
Expand Up @@ -11,6 +11,7 @@
capture_internal_exceptions,
event_from_exception,
format_timestamp,
parse_version,
)

try:
Expand Down Expand Up @@ -39,9 +40,9 @@ class RqIntegration(Integration):
def setup_once():
# type: () -> None

try:
version = tuple(map(int, RQ_VERSION.split(".")[:3]))
except (ValueError, TypeError):
version = parse_version(RQ_VERSION)

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

if version < (0, 6):
Expand Down
11 changes: 6 additions & 5 deletions sentry_sdk/integrations/sanic.py
Expand Up @@ -10,6 +10,7 @@
event_from_exception,
HAS_REAL_CONTEXTVARS,
CONTEXTVARS_ERROR_MESSAGE,
parse_version,
)
from sentry_sdk.integrations import Integration, DidNotEnable
from sentry_sdk.integrations._wsgi_common import RequestExtractor, _filter_headers
Expand Down Expand Up @@ -51,15 +52,15 @@

class SanicIntegration(Integration):
identifier = "sanic"
version = (0, 0) # type: Tuple[int, ...]
version = None

@staticmethod
def setup_once():
# type: () -> None

try:
SanicIntegration.version = tuple(map(int, SANIC_VERSION.split(".")))
except (TypeError, ValueError):
SanicIntegration.version = parse_version(SANIC_VERSION)

if SanicIntegration.version is None:
raise DidNotEnable("Unparsable Sanic version: {}".format(SANIC_VERSION))

if SanicIntegration.version < (0, 8):
Expand Down Expand Up @@ -225,7 +226,7 @@ async def sentry_wrapped_error_handler(request, exception):
finally:
# As mentioned in previous comment in _startup, this can be removed
# after https://github.com/sanic-org/sanic/issues/2297 is resolved
if SanicIntegration.version == (21, 9):
if SanicIntegration.version and SanicIntegration.version == (21, 9):
await _hub_exit(request)

return sentry_wrapped_error_handler
Expand Down
12 changes: 5 additions & 7 deletions sentry_sdk/integrations/sqlalchemy.py
@@ -1,14 +1,14 @@
from __future__ import absolute_import

import re

from sentry_sdk._compat import text_type
from sentry_sdk._types import TYPE_CHECKING
from sentry_sdk.consts import SPANDATA
from sentry_sdk.hub import Hub
from sentry_sdk.integrations import Integration, DidNotEnable
from sentry_sdk.tracing_utils import record_sql_queries

from sentry_sdk.utils import parse_version

try:
from sqlalchemy.engine import Engine # type: ignore
from sqlalchemy.event import listen # type: ignore
Expand All @@ -31,11 +31,9 @@ class SqlalchemyIntegration(Integration):
def setup_once():
# type: () -> None

try:
version = tuple(
map(int, re.split("b|rc", SQLALCHEMY_VERSION)[0].split("."))
)
except (TypeError, ValueError):
version = parse_version(SQLALCHEMY_VERSION)

if version is None:
raise DidNotEnable(
"Unparsable SQLAlchemy version: {}".format(SQLALCHEMY_VERSION)
)
Expand Down
52 changes: 52 additions & 0 deletions sentry_sdk/utils.py
Expand Up @@ -1469,6 +1469,58 @@ def match_regex_list(item, regex_list=None, substring_matching=False):
return False


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


if PY37:

def nanosecond_time():
Expand Down
37 changes: 37 additions & 0 deletions tests/test_utils.py
Expand Up @@ -7,6 +7,7 @@
logger,
match_regex_list,
parse_url,
parse_version,
sanitize_url,
serialize_frame,
)
Expand Down Expand Up @@ -263,3 +264,39 @@ def test_include_source_context_when_serializing_frame(include_source_context):
)
def test_match_regex_list(item, regex_list, expected_result):
assert match_regex_list(item, regex_list) == expected_result


@pytest.mark.parametrize(
"version,expected_result",
[
["3.5.15", (3, 5, 15)],
["2.0.9", (2, 0, 9)],
["2.0.0", (2, 0, 0)],
["0.6.0", (0, 6, 0)],
["2.0.0.post1", (2, 0, 0)],
["2.0.0rc3", (2, 0, 0)],
["2.0.0rc2", (2, 0, 0)],
["2.0.0rc1", (2, 0, 0)],
["2.0.0b4", (2, 0, 0)],
["2.0.0b3", (2, 0, 0)],
["2.0.0b2", (2, 0, 0)],
["2.0.0b1", (2, 0, 0)],
["0.6beta3", (0, 6)],
["0.6beta2", (0, 6)],
["0.6beta1", (0, 6)],
["0.4.2b", (0, 4, 2)],
["0.4.2a", (0, 4, 2)],
["0.0.1", (0, 0, 1)],
["0.0.0", (0, 0, 0)],
["1", (1,)],
["1.0", (1, 0)],
["1.0.0", (1, 0, 0)],
[" 1.0.0 ", (1, 0, 0)],
[" 1.0.0 ", (1, 0, 0)],
["x1.0.0", None],
["1.0.0x", None],
["x1.0.0x", None],
],
)
def test_parse_version(version, expected_result):
assert parse_version(version) == expected_result

0 comments on commit 87eb761

Please sign in to comment.