Repository: getsentry/sentry-python

Issue:
Sentry-SDK 1.33 fails to import with "parse_version` is undefined with greenlet installed
### How do you use Sentry?

Sentry Saas (sentry.io)

### Version

1.33.0

### Steps to Reproduce

1. Install sentry-sdk 1.32 and greenlet:

```
root@99404168d77f:/opt/airflow# pip freeze | grep -E "greenlet|sentry-sdk"
greenlet==3.0.1
sentry-sdk==1.32.0
root@99404168d77f:/opt/airflow# 
```

2. Import sentry_sdk

root@99404168d77f:/opt/airflow# python
Python 3.8.18 (default, Oct 11 2023, 23:57:43) 
[GCC 10.2.1 20210110] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import sentry_sdk
>>> 

Works fine

3. Upgrade to sentry_sdk 1.33.0

```
root@99404168d77f:/opt/airflow# pip install --upgrade sentry-sdk==1.33.0
Collecting sentry-sdk==1.33.0
  Downloading sentry_sdk-1.33.0-py2.py3-none-any.whl.metadata (9.6 kB)
Requirement already satisfied: certifi in /usr/local/lib/python3.8/site-packages (from sentry-sdk==1.33.0) (2023.7.22)
Requirement already satisfied: urllib3>=1.26.11 in /usr/local/lib/python3.8/site-packages (from sentry-sdk==1.33.0) (1.26.18)
Downloading sentry_sdk-1.33.0-py2.py3-none-any.whl (243 kB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 243.2/243.2 kB 2.8 MB/s eta 0:00:00
Installing collected packages: sentry-sdk
  Attempting uninstall: sentry-sdk
    Found existing installation: sentry-sdk 1.32.0
    Uninstalling sentry-sdk-1.32.0:
      Successfully uninstalled sentry-sdk-1.32.0
Successfully installed sentry-sdk-1.33.0
```

```
root@99404168d77f:/opt/airflow# pip freeze | grep -E "greenlet|sentry-sdk"
greenlet==3.0.1
sentry-sdk==1.33.0
root@99404168d77f:/opt/airflow# 
```

4. Import sentry_sdk:

```
root@99404168d77f:/opt/airflow# python
Python 3.8.18 (default, Oct 11 2023, 23:57:43) 
[GCC 10.2.1 20210110] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import sentry_sdk
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python3.8/site-packages/sentry_sdk/__init__.py", line 1, in <module>
    from sentry_sdk.hub import Hub, init
  File "/usr/local/lib/python3.8/site-packages/sentry_sdk/hub.py", line 8, in <module>
    from sentry_sdk.scope import Scope
  File "/usr/local/lib/python3.8/site-packages/sentry_sdk/scope.py", line 7, in <module>
    from sentry_sdk.attachments import Attachment
  File "/usr/local/lib/python3.8/site-packages/sentry_sdk/attachments.py", line 5, in <module>
    from sentry_sdk.envelope import Item, PayloadRef
  File "/usr/local/lib/python3.8/site-packages/sentry_sdk/envelope.py", line 7, in <module>
    from sentry_sdk.session import Session
  File "/usr/local/lib/python3.8/site-packages/sentry_sdk/session.py", line 5, in <module>
    from sentry_sdk.utils import format_timestamp
  File "/usr/local/lib/python3.8/site-packages/sentry_sdk/utils.py", line 1240, in <module>
    HAS_REAL_CONTEXTVARS, ContextVar = _get_contextvars()
  File "/usr/local/lib/python3.8/site-packages/sentry_sdk/utils.py", line 1210, in _get_contextvars
    if not _is_contextvars_broken():
  File "/usr/local/lib/python3.8/site-packages/sentry_sdk/utils.py", line 1165, in _is_contextvars_broken
    greenlet_version = parse_version(greenlet.__version__)
NameError: name 'parse_version' is not defined
>>> 
```







### Expected Result

The import should succeed.

### Actual Result

The import fails with exception:

```
root@99404168d77f:/opt/airflow# python
Python 3.8.18 (default, Oct 11 2023, 23:57:43) 
[GCC 10.2.1 20210110] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import sentry_sdk
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python3.8/site-packages/sentry_sdk/__init__.py", line 1, in <module>
    from sentry_sdk.hub import Hub, init
  File "/usr/local/lib/python3.8/site-packages/sentry_sdk/hub.py", line 8, in <module>
    from sentry_sdk.scope import Scope
  File "/usr/local/lib/python3.8/site-packages/sentry_sdk/scope.py", line 7, in <module>
    from sentry_sdk.attachments import Attachment
  File "/usr/local/lib/python3.8/site-packages/sentry_sdk/attachments.py", line 5, in <module>
    from sentry_sdk.envelope import Item, PayloadRef
  File "/usr/local/lib/python3.8/site-packages/sentry_sdk/envelope.py", line 7, in <module>
    from sentry_sdk.session import Session
  File "/usr/local/lib/python3.8/site-packages/sentry_sdk/session.py", line 5, in <module>
    from sentry_sdk.utils import format_timestamp
  File "/usr/local/lib/python3.8/site-packages/sentry_sdk/utils.py", line 1240, in <module>
    HAS_REAL_CONTEXTVARS, ContextVar = _get_contextvars()
  File "/usr/local/lib/python3.8/site-packages/sentry_sdk/utils.py", line 1210, in _get_contextvars
    if not _is_contextvars_broken():
  File "/usr/local/lib/python3.8/site-packages/sentry_sdk/utils.py", line 1165, in _is_contextvars_broken
    greenlet_version = parse_version(greenlet.__version__)
NameError: name 'parse_version' is not defined
>>> 
```


Discussion:
Very likely caused by https://github.com/getsentry/sentry-python/pull/2464

Resolving patch:
diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py
--- a/sentry_sdk/utils.py
+++ b/sentry_sdk/utils.py
@@ -1126,6 +1126,58 @@ 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
     """
@@ -1520,58 +1572,6 @@ 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
-
-
 if PY37:
 
     def nanosecond_time():


Test patch: