Skip to content

Commit 87eb761

Browse files
authored
Better version parsing in integrations (#2152)
1 parent 692d099 commit 87eb761

File tree

12 files changed

+147
-45
lines changed

12 files changed

+147
-45
lines changed

sentry_sdk/integrations/aiohttp.py

+5-4
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from sentry_sdk.utils import (
1616
capture_internal_exceptions,
1717
event_from_exception,
18+
parse_version,
1819
transaction_from_function,
1920
HAS_REAL_CONTEXTVARS,
2021
CONTEXTVARS_ERROR_MESSAGE,
@@ -64,10 +65,10 @@ def __init__(self, transaction_style="handler_name"):
6465
def setup_once():
6566
# type: () -> None
6667

67-
try:
68-
version = tuple(map(int, AIOHTTP_VERSION.split(".")[:2]))
69-
except (TypeError, ValueError):
70-
raise DidNotEnable("AIOHTTP version unparsable: {}".format(AIOHTTP_VERSION))
68+
version = parse_version(AIOHTTP_VERSION)
69+
70+
if version is None:
71+
raise DidNotEnable("Unparsable AIOHTTP version: {}".format(AIOHTTP_VERSION))
7172

7273
if version < (3, 4):
7374
raise DidNotEnable("AIOHTTP 3.4 or newer required.")

sentry_sdk/integrations/arq.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
capture_internal_exceptions,
1515
event_from_exception,
1616
SENSITIVE_DATA_SUBSTITUTE,
17+
parse_version,
1718
)
1819

1920
try:
@@ -45,11 +46,15 @@ def setup_once():
4546

4647
try:
4748
if isinstance(ARQ_VERSION, str):
48-
version = tuple(map(int, ARQ_VERSION.split(".")[:2]))
49+
version = parse_version(ARQ_VERSION)
4950
else:
5051
version = ARQ_VERSION.version[:2]
52+
5153
except (TypeError, ValueError):
52-
raise DidNotEnable("arq version unparsable: {}".format(ARQ_VERSION))
54+
version = None
55+
56+
if version is None:
57+
raise DidNotEnable("Unparsable arq version: {}".format(ARQ_VERSION))
5358

5459
if version < (0, 23):
5560
raise DidNotEnable("arq 0.23 or newer required.")

sentry_sdk/integrations/boto3.py

+7-4
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from sentry_sdk._functools import partial
99
from sentry_sdk._types import TYPE_CHECKING
10-
from sentry_sdk.utils import parse_url
10+
from sentry_sdk.utils import parse_url, parse_version
1111

1212
if TYPE_CHECKING:
1313
from typing import Any
@@ -30,14 +30,17 @@ class Boto3Integration(Integration):
3030
@staticmethod
3131
def setup_once():
3232
# type: () -> None
33-
try:
34-
version = tuple(map(int, BOTOCORE_VERSION.split(".")[:3]))
35-
except (ValueError, TypeError):
33+
34+
version = parse_version(BOTOCORE_VERSION)
35+
36+
if version is None:
3637
raise DidNotEnable(
3738
"Unparsable botocore version: {}".format(BOTOCORE_VERSION)
3839
)
40+
3941
if version < (1, 12):
4042
raise DidNotEnable("Botocore 1.12 or newer is required.")
43+
4144
orig_init = BaseClient.__init__
4245

4346
def sentry_patched_init(self, *args, **kwargs):

sentry_sdk/integrations/bottle.py

+5-4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from sentry_sdk.utils import (
66
capture_internal_exceptions,
77
event_from_exception,
8+
parse_version,
89
transaction_from_function,
910
)
1011
from sentry_sdk.integrations import Integration, DidNotEnable
@@ -57,10 +58,10 @@ def __init__(self, transaction_style="endpoint"):
5758
def setup_once():
5859
# type: () -> None
5960

60-
try:
61-
version = tuple(map(int, BOTTLE_VERSION.replace("-dev", "").split(".")))
62-
except (TypeError, ValueError):
63-
raise DidNotEnable("Unparsable Bottle version: {}".format(version))
61+
version = parse_version(BOTTLE_VERSION)
62+
63+
if version is None:
64+
raise DidNotEnable("Unparsable Bottle version: {}".format(BOTTLE_VERSION))
6465

6566
if version < (0, 12):
6667
raise DidNotEnable("Bottle 0.12 or newer required.")

sentry_sdk/integrations/chalice.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from sentry_sdk.utils import (
99
capture_internal_exceptions,
1010
event_from_exception,
11+
parse_version,
1112
)
1213
from sentry_sdk._types import TYPE_CHECKING
1314
from sentry_sdk._functools import wraps
@@ -102,10 +103,12 @@ class ChaliceIntegration(Integration):
102103
@staticmethod
103104
def setup_once():
104105
# type: () -> None
105-
try:
106-
version = tuple(map(int, CHALICE_VERSION.split(".")[:3]))
107-
except (ValueError, TypeError):
106+
107+
version = parse_version(CHALICE_VERSION)
108+
109+
if version is None:
108110
raise DidNotEnable("Unparsable Chalice version: {}".format(CHALICE_VERSION))
111+
109112
if version < (1, 20):
110113
old_get_view_function_response = Chalice._get_view_function_response
111114
else:

sentry_sdk/integrations/falcon.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from sentry_sdk.utils import (
99
capture_internal_exceptions,
1010
event_from_exception,
11+
parse_version,
1112
)
1213

1314
from sentry_sdk._types import TYPE_CHECKING
@@ -131,9 +132,10 @@ def __init__(self, transaction_style="uri_template"):
131132
@staticmethod
132133
def setup_once():
133134
# type: () -> None
134-
try:
135-
version = tuple(map(int, FALCON_VERSION.split(".")))
136-
except (ValueError, TypeError):
135+
136+
version = parse_version(FALCON_VERSION)
137+
138+
if version is None:
137139
raise DidNotEnable("Unparsable Falcon version: {}".format(FALCON_VERSION))
138140

139141
if version < (1, 4):

sentry_sdk/integrations/flask.py

+8-10
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from sentry_sdk.utils import (
1111
capture_internal_exceptions,
1212
event_from_exception,
13+
parse_version,
1314
)
1415

1516
if TYPE_CHECKING:
@@ -64,16 +65,13 @@ def __init__(self, transaction_style="endpoint"):
6465
def setup_once():
6566
# type: () -> None
6667

67-
# This version parsing is absolutely naive but the alternative is to
68-
# import pkg_resources which slows down the SDK a lot.
69-
try:
70-
version = tuple(map(int, FLASK_VERSION.split(".")[:3]))
71-
except (ValueError, TypeError):
72-
# It's probably a release candidate, we assume it's fine.
73-
pass
74-
else:
75-
if version < (0, 10):
76-
raise DidNotEnable("Flask 0.10 or newer is required.")
68+
version = parse_version(FLASK_VERSION)
69+
70+
if version is None:
71+
raise DidNotEnable("Unparsable Flask version: {}".format(FLASK_VERSION))
72+
73+
if version < (0, 10):
74+
raise DidNotEnable("Flask 0.10 or newer is required.")
7775

7876
before_render_template.connect(_add_sentry_trace)
7977
request_started.connect(_request_started)

sentry_sdk/integrations/rq.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
capture_internal_exceptions,
1212
event_from_exception,
1313
format_timestamp,
14+
parse_version,
1415
)
1516

1617
try:
@@ -39,9 +40,9 @@ class RqIntegration(Integration):
3940
def setup_once():
4041
# type: () -> None
4142

42-
try:
43-
version = tuple(map(int, RQ_VERSION.split(".")[:3]))
44-
except (ValueError, TypeError):
43+
version = parse_version(RQ_VERSION)
44+
45+
if version is None:
4546
raise DidNotEnable("Unparsable RQ version: {}".format(RQ_VERSION))
4647

4748
if version < (0, 6):

sentry_sdk/integrations/sanic.py

+6-5
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
event_from_exception,
1111
HAS_REAL_CONTEXTVARS,
1212
CONTEXTVARS_ERROR_MESSAGE,
13+
parse_version,
1314
)
1415
from sentry_sdk.integrations import Integration, DidNotEnable
1516
from sentry_sdk.integrations._wsgi_common import RequestExtractor, _filter_headers
@@ -51,15 +52,15 @@
5152

5253
class SanicIntegration(Integration):
5354
identifier = "sanic"
54-
version = (0, 0) # type: Tuple[int, ...]
55+
version = None
5556

5657
@staticmethod
5758
def setup_once():
5859
# type: () -> None
5960

60-
try:
61-
SanicIntegration.version = tuple(map(int, SANIC_VERSION.split(".")))
62-
except (TypeError, ValueError):
61+
SanicIntegration.version = parse_version(SANIC_VERSION)
62+
63+
if SanicIntegration.version is None:
6364
raise DidNotEnable("Unparsable Sanic version: {}".format(SANIC_VERSION))
6465

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

231232
return sentry_wrapped_error_handler

sentry_sdk/integrations/sqlalchemy.py

+5-7
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
from __future__ import absolute_import
22

3-
import re
4-
53
from sentry_sdk._compat import text_type
64
from sentry_sdk._types import TYPE_CHECKING
75
from sentry_sdk.consts import SPANDATA
86
from sentry_sdk.hub import Hub
97
from sentry_sdk.integrations import Integration, DidNotEnable
108
from sentry_sdk.tracing_utils import record_sql_queries
119

10+
from sentry_sdk.utils import parse_version
11+
1212
try:
1313
from sqlalchemy.engine import Engine # type: ignore
1414
from sqlalchemy.event import listen # type: ignore
@@ -31,11 +31,9 @@ class SqlalchemyIntegration(Integration):
3131
def setup_once():
3232
# type: () -> None
3333

34-
try:
35-
version = tuple(
36-
map(int, re.split("b|rc", SQLALCHEMY_VERSION)[0].split("."))
37-
)
38-
except (TypeError, ValueError):
34+
version = parse_version(SQLALCHEMY_VERSION)
35+
36+
if version is None:
3937
raise DidNotEnable(
4038
"Unparsable SQLAlchemy version: {}".format(SQLALCHEMY_VERSION)
4139
)

sentry_sdk/utils.py

+52
Original file line numberDiff line numberDiff line change
@@ -1469,6 +1469,58 @@ def match_regex_list(item, regex_list=None, substring_matching=False):
14691469
return False
14701470

14711471

1472+
def parse_version(version):
1473+
# type: (str) -> Optional[Tuple[int, ...]]
1474+
"""
1475+
Parses a version string into a tuple of integers.
1476+
This uses the parsing loging from PEP 440:
1477+
https://peps.python.org/pep-0440/#appendix-b-parsing-version-strings-with-regular-expressions
1478+
"""
1479+
VERSION_PATTERN = r""" # noqa: N806
1480+
v?
1481+
(?:
1482+
(?:(?P<epoch>[0-9]+)!)? # epoch
1483+
(?P<release>[0-9]+(?:\.[0-9]+)*) # release segment
1484+
(?P<pre> # pre-release
1485+
[-_\.]?
1486+
(?P<pre_l>(a|b|c|rc|alpha|beta|pre|preview))
1487+
[-_\.]?
1488+
(?P<pre_n>[0-9]+)?
1489+
)?
1490+
(?P<post> # post release
1491+
(?:-(?P<post_n1>[0-9]+))
1492+
|
1493+
(?:
1494+
[-_\.]?
1495+
(?P<post_l>post|rev|r)
1496+
[-_\.]?
1497+
(?P<post_n2>[0-9]+)?
1498+
)
1499+
)?
1500+
(?P<dev> # dev release
1501+
[-_\.]?
1502+
(?P<dev_l>dev)
1503+
[-_\.]?
1504+
(?P<dev_n>[0-9]+)?
1505+
)?
1506+
)
1507+
(?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))? # local version
1508+
"""
1509+
1510+
pattern = re.compile(
1511+
r"^\s*" + VERSION_PATTERN + r"\s*$",
1512+
re.VERBOSE | re.IGNORECASE,
1513+
)
1514+
1515+
try:
1516+
release = pattern.match(version).groupdict()["release"] # type: ignore
1517+
release_tuple = tuple(map(int, release.split(".")[:3])) # type: Tuple[int, ...]
1518+
except (TypeError, ValueError, AttributeError):
1519+
return None
1520+
1521+
return release_tuple
1522+
1523+
14721524
if PY37:
14731525

14741526
def nanosecond_time():

tests/test_utils.py

+37
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
logger,
88
match_regex_list,
99
parse_url,
10+
parse_version,
1011
sanitize_url,
1112
serialize_frame,
1213
)
@@ -263,3 +264,39 @@ def test_include_source_context_when_serializing_frame(include_source_context):
263264
)
264265
def test_match_regex_list(item, regex_list, expected_result):
265266
assert match_regex_list(item, regex_list) == expected_result
267+
268+
269+
@pytest.mark.parametrize(
270+
"version,expected_result",
271+
[
272+
["3.5.15", (3, 5, 15)],
273+
["2.0.9", (2, 0, 9)],
274+
["2.0.0", (2, 0, 0)],
275+
["0.6.0", (0, 6, 0)],
276+
["2.0.0.post1", (2, 0, 0)],
277+
["2.0.0rc3", (2, 0, 0)],
278+
["2.0.0rc2", (2, 0, 0)],
279+
["2.0.0rc1", (2, 0, 0)],
280+
["2.0.0b4", (2, 0, 0)],
281+
["2.0.0b3", (2, 0, 0)],
282+
["2.0.0b2", (2, 0, 0)],
283+
["2.0.0b1", (2, 0, 0)],
284+
["0.6beta3", (0, 6)],
285+
["0.6beta2", (0, 6)],
286+
["0.6beta1", (0, 6)],
287+
["0.4.2b", (0, 4, 2)],
288+
["0.4.2a", (0, 4, 2)],
289+
["0.0.1", (0, 0, 1)],
290+
["0.0.0", (0, 0, 0)],
291+
["1", (1,)],
292+
["1.0", (1, 0)],
293+
["1.0.0", (1, 0, 0)],
294+
[" 1.0.0 ", (1, 0, 0)],
295+
[" 1.0.0 ", (1, 0, 0)],
296+
["x1.0.0", None],
297+
["1.0.0x", None],
298+
["x1.0.0x", None],
299+
],
300+
)
301+
def test_parse_version(version, expected_result):
302+
assert parse_version(version) == expected_result

0 commit comments

Comments
 (0)