From 897e8d5a178806c445b604881e9b5ac79deca397 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 29 Jul 2026 11:18:47 +0200 Subject: [PATCH 1/6] ref: Remove contextvars compatibility code Drop the contextvars compatibility layer that supported Python 3.6 (aiocontextvars), old gevent (<20.9.0), and old greenlet (<0.5). - Remove `_is_contextvars_broken`, `_make_threadlocal_contextvars`, `_get_contextvars`, `HAS_REAL_CONTEXTVARS`, and `CONTEXTVARS_ERROR_MESSAGE` from utils.py - Use `from contextvars import ContextVar` directly everywhere - Remove `unsafe_context_data` parameter from `SentryAsgiMiddleware` - Remove contextvars checks from aiohttp, asgi, tornado, sanic, and django integrations - Add version deprecation warnings for gevent <20.9.0 and greenlet <0.5 in `_check_version_deprecations()` - Add type annotations to all ContextVar declarations --- sentry_sdk/_init_implementation.py | 37 +++++- sentry_sdk/ai/monitoring.py | 7 +- sentry_sdk/client.py | 4 +- sentry_sdk/hub.py | 8 +- sentry_sdk/integrations/aiohttp.py | 10 -- sentry_sdk/integrations/asgi.py | 17 +-- sentry_sdk/integrations/dedupe.py | 7 +- sentry_sdk/integrations/django/__init__.py | 26 ---- sentry_sdk/integrations/django/asgi.py | 2 - sentry_sdk/integrations/django/middleware.py | 4 +- sentry_sdk/integrations/litestar.py | 1 - sentry_sdk/integrations/sanic.py | 10 -- sentry_sdk/integrations/starlite.py | 1 - sentry_sdk/integrations/tornado.py | 10 -- sentry_sdk/integrations/wsgi.py | 6 +- sentry_sdk/scope.py | 10 +- sentry_sdk/utils.py | 131 +------------------ tests/utils/test_contextvars.py | 22 +--- 18 files changed, 67 insertions(+), 246 deletions(-) diff --git a/sentry_sdk/_init_implementation.py b/sentry_sdk/_init_implementation.py index 923fcf6df8..f889702512 100644 --- a/sentry_sdk/_init_implementation.py +++ b/sentry_sdk/_init_implementation.py @@ -41,11 +41,36 @@ def __exit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None: c.close() -def _check_python_deprecations() -> None: - # Since we're likely to deprecate Python versions in the future, I'm keeping - # this handy function around. Use this to detect the Python version used and - # to output logger.warning()s if it's deprecated. - pass +def _check_version_deprecations() -> None: + import re + + from sentry_sdk.utils import logger, parse_version + + try: + import gevent + + gevent_version = tuple( + int(part) for part in re.split(r"a|b|rc|\.", gevent.__version__)[:2] + ) + if gevent_version < (20, 9): + logger.warning( + "sentry-sdk 3.x supports gevent 20.9.0 or newer. " + "Please upgrade gevent or downgrade to sentry-sdk 2.x." + ) + except ImportError: + pass + + try: + import greenlet + + greenlet_version = parse_version(greenlet.__version__) + if greenlet_version is not None and greenlet_version < (0, 5): + logger.warning( + "sentry-sdk 3.x supports greenlet 0.5 or newer. " + "Please upgrade greenlet or downgrade to sentry-sdk 2.x." + ) + except ImportError: + pass def _init(*args: "Optional[str]", **kwargs: "Any") -> "ContextManager[Any]": @@ -55,7 +80,7 @@ def _init(*args: "Optional[str]", **kwargs: "Any") -> "ContextManager[Any]": """ client = sentry_sdk.Client(*args, **kwargs) sentry_sdk.get_global_scope().set_client(client) - _check_python_deprecations() + _check_version_deprecations() rv = _InitGuard(client) return rv diff --git a/sentry_sdk/ai/monitoring.py b/sentry_sdk/ai/monitoring.py index 423bb50f9d..30c1a64d9a 100644 --- a/sentry_sdk/ai/monitoring.py +++ b/sentry_sdk/ai/monitoring.py @@ -1,5 +1,6 @@ import inspect import sys +from contextvars import ContextVar from functools import wraps from typing import TYPE_CHECKING @@ -10,14 +11,16 @@ from sentry_sdk.traces import StreamedSpan from sentry_sdk.tracing import Span from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import ContextVar, capture_internal_exceptions, reraise +from sentry_sdk.utils import capture_internal_exceptions, reraise if TYPE_CHECKING: from typing import Any, Awaitable, Callable, Optional, TypeVar, Union F = TypeVar("F", bound=Union[Callable[..., Any], Callable[..., Awaitable[Any]]]) -_ai_pipeline_name = ContextVar("ai_pipeline_name", default=None) +_ai_pipeline_name: "ContextVar[Optional[str]]" = ContextVar( + "ai_pipeline_name", default=None +) def set_ai_pipeline_name(name: "Optional[str]") -> None: diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index e8414d3f91..2168eabb9a 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -7,6 +7,7 @@ import uuid import warnings from collections.abc import Iterable, Mapping +from contextvars import ContextVar from datetime import datetime, timezone from importlib import import_module from typing import TYPE_CHECKING, Dict, List, cast, overload @@ -50,7 +51,6 @@ ) from sentry_sdk.utils import ( AnnotatedValue, - ContextVar, capture_internal_exceptions, current_stacktrace, datetime_from_isoformat, @@ -93,7 +93,7 @@ I = TypeVar("I", bound=Integration) # noqa: E741 -_client_init_debug = ContextVar("client_init_debug") +_client_init_debug: "ContextVar[bool]" = ContextVar("client_init_debug") SDK_INFO: "SDKInfo" = { diff --git a/sentry_sdk/hub.py b/sentry_sdk/hub.py index b17444d06e..315f583c0a 100644 --- a/sentry_sdk/hub.py +++ b/sentry_sdk/hub.py @@ -1,5 +1,6 @@ import warnings from contextlib import contextmanager +from contextvars import ContextVar from typing import TYPE_CHECKING from sentry_sdk import ( @@ -17,10 +18,7 @@ Span, Transaction, ) -from sentry_sdk.utils import ( - ContextVar, - logger, -) +from sentry_sdk.utils import logger if TYPE_CHECKING: from typing import ( @@ -86,7 +84,7 @@ def _suppress_hub_deprecation_warning() -> "Generator[None, None, None]": yield -_local = ContextVar("sentry_current_hub") +_local: "ContextVar[Optional[Hub]]" = ContextVar("sentry_current_hub") class HubMeta(type): diff --git a/sentry_sdk/integrations/aiohttp.py b/sentry_sdk/integrations/aiohttp.py index 858bf273f2..eac6cb901a 100644 --- a/sentry_sdk/integrations/aiohttp.py +++ b/sentry_sdk/integrations/aiohttp.py @@ -41,8 +41,6 @@ should_propagate_trace, ) from sentry_sdk.utils import ( - CONTEXTVARS_ERROR_MESSAGE, - HAS_REAL_CONTEXTVARS, SENSITIVE_DATA_SUBSTITUTE, AnnotatedValue, _register_control_flow_exception, @@ -108,14 +106,6 @@ def setup_once() -> None: version = parse_version(AIOHTTP_VERSION) _check_minimum_version(AioHttpIntegration, version) - if not HAS_REAL_CONTEXTVARS: - # We better have contextvars or we're going to leak state between - # requests. - raise DidNotEnable( - "The aiohttp integration for Sentry requires Python 3.7+ " - " or aiocontextvars package." + CONTEXTVARS_ERROR_MESSAGE - ) - # In the aiohttp integration, all of their HTTP responses are Exceptions. # Because they have to be raised and handled by the framework, we need to # register the exceptions as control flow exceptions so that we don't diff --git a/sentry_sdk/integrations/asgi.py b/sentry_sdk/integrations/asgi.py index 9921e6c1ff..eb06c9c49d 100644 --- a/sentry_sdk/integrations/asgi.py +++ b/sentry_sdk/integrations/asgi.py @@ -6,6 +6,7 @@ import inspect import sys +from contextvars import ContextVar from copy import deepcopy from functools import partial from typing import TYPE_CHECKING @@ -41,9 +42,6 @@ ) from sentry_sdk.tracing_utils import has_span_streaming_enabled from sentry_sdk.utils import ( - CONTEXTVARS_ERROR_MESSAGE, - HAS_REAL_CONTEXTVARS, - ContextVar, _get_installed_modules, capture_internal_exceptions, event_from_exception, @@ -61,7 +59,9 @@ from sentry_sdk.tracing import Span -_asgi_middleware_applied = ContextVar("sentry_asgi_middleware_applied") +_asgi_middleware_applied: "ContextVar[bool]" = ContextVar( + "sentry_asgi_middleware_applied" +) _DEFAULT_TRANSACTION_NAME = "generic ASGI request" @@ -113,7 +113,6 @@ class SentryAsgiMiddleware: def __init__( self, app: "Any", - unsafe_context_data: bool = False, transaction_style: str = "endpoint", mechanism_type: str = "asgi", span_origin: str = "manual", @@ -126,15 +125,7 @@ def __init__( data to sent events and basic handling for exceptions bubbling up through the middleware. - :param unsafe_context_data: Disable errors when a proper contextvars installation could not be found. We do not recommend changing this from the default. """ - if not unsafe_context_data and not HAS_REAL_CONTEXTVARS: - # We better have contextvars or we're going to leak state between - # requests. - raise RuntimeError( - "The ASGI middleware for Sentry requires Python 3.7+ " - "or the aiocontextvars package." + CONTEXTVARS_ERROR_MESSAGE - ) if transaction_style not in TRANSACTION_STYLE_VALUES: raise ValueError( "Invalid value for transaction_style: %s (must be in %s)" diff --git a/sentry_sdk/integrations/dedupe.py b/sentry_sdk/integrations/dedupe.py index a0e9014666..a0cc88081f 100644 --- a/sentry_sdk/integrations/dedupe.py +++ b/sentry_sdk/integrations/dedupe.py @@ -1,13 +1,14 @@ import weakref +from contextvars import ContextVar from typing import TYPE_CHECKING import sentry_sdk from sentry_sdk.integrations import Integration from sentry_sdk.scope import add_global_event_processor -from sentry_sdk.utils import ContextVar, logger +from sentry_sdk.utils import logger if TYPE_CHECKING: - from typing import Optional + from typing import Any, Optional from sentry_sdk._types import Event, Hint @@ -16,7 +17,7 @@ class DedupeIntegration(Integration): identifier = "dedupe" def __init__(self) -> None: - self._last_seen = ContextVar("last-seen") + self._last_seen: "ContextVar[Any]" = ContextVar("last-seen") @staticmethod def setup_once() -> None: diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index 0f8dfcd24a..b5bd550be5 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -23,15 +23,12 @@ record_sql_queries, ) from sentry_sdk.utils import ( - CONTEXTVARS_ERROR_MESSAGE, - HAS_REAL_CONTEXTVARS, SENSITIVE_DATA_SUBSTITUTE, AnnotatedValue, capture_internal_exceptions, ensure_integration_enabled, event_from_exception, has_data_collection_enabled, - logger, transaction_from_function, walk_exception_chain, ) @@ -346,19 +343,6 @@ def _patch_channels() -> None: except ImportError: return - if not HAS_REAL_CONTEXTVARS: - # We better have contextvars or we're going to leak state between - # requests. - # - # We cannot hard-raise here because channels may not be used at all in - # the current process. That is the case when running traditional WSGI - # workers in gunicorn+gevent and the websocket stuff in a separate - # process. - logger.warning( - "We detected that you are using Django channels 2.0." - + CONTEXTVARS_ERROR_MESSAGE - ) - from sentry_sdk.integrations.django.asgi import patch_channels_asgi_handler_impl patch_channels_asgi_handler_impl(AsgiHandler) @@ -370,16 +354,6 @@ def _patch_django_asgi_handler() -> None: except ImportError: return - if not HAS_REAL_CONTEXTVARS: - # We better have contextvars or we're going to leak state between - # requests. - # - # We cannot hard-raise here because Django's ASGI stuff may not be used - # at all. - logger.warning( - "We detected that you are using Django 3." + CONTEXTVARS_ERROR_MESSAGE - ) - from sentry_sdk.integrations.django.asgi import patch_django_asgi_handler_impl patch_django_asgi_handler_impl(ASGIHandler) diff --git a/sentry_sdk/integrations/django/asgi.py b/sentry_sdk/integrations/django/asgi.py index df287e6436..c5d52fe72d 100644 --- a/sentry_sdk/integrations/django/asgi.py +++ b/sentry_sdk/integrations/django/asgi.py @@ -99,7 +99,6 @@ async def sentry_patched_asgi_handler( middleware = SentryAsgiMiddleware( old_app.__get__(self, cls), - unsafe_context_data=True, span_origin=DjangoIntegration.origin, http_methods_to_capture=integration.http_methods_to_capture, )._run_asgi3 @@ -154,7 +153,6 @@ async def sentry_patched_asgi_handler( middleware = SentryAsgiMiddleware( lambda _scope: old_app.__get__(self, cls), - unsafe_context_data=True, span_origin=DjangoIntegration.origin, http_methods_to_capture=integration.http_methods_to_capture, ) diff --git a/sentry_sdk/integrations/django/middleware.py b/sentry_sdk/integrations/django/middleware.py index 01ed8962e0..dcd114e795 100644 --- a/sentry_sdk/integrations/django/middleware.py +++ b/sentry_sdk/integrations/django/middleware.py @@ -2,6 +2,7 @@ Create spans from Django middleware invocations """ +from contextvars import ContextVar from functools import wraps from typing import TYPE_CHECKING @@ -11,7 +12,6 @@ from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.tracing_utils import has_span_streaming_enabled from sentry_sdk.utils import ( - ContextVar, capture_internal_exceptions, transaction_from_function, ) @@ -24,7 +24,7 @@ F = TypeVar("F", bound=Callable[..., Any]) -_import_string_should_wrap_middleware = ContextVar( +_import_string_should_wrap_middleware: "ContextVar[bool]" = ContextVar( "import_string_should_wrap_middleware" ) diff --git a/sentry_sdk/integrations/litestar.py b/sentry_sdk/integrations/litestar.py index 8a3f09ffa0..dd80505132 100644 --- a/sentry_sdk/integrations/litestar.py +++ b/sentry_sdk/integrations/litestar.py @@ -89,7 +89,6 @@ def __init__( ) -> None: super().__init__( app=app, - unsafe_context_data=False, transaction_style="endpoint", mechanism_type="asgi", span_origin=span_origin, diff --git a/sentry_sdk/integrations/sanic.py b/sentry_sdk/integrations/sanic.py index 2d839d1c61..d1619960ed 100644 --- a/sentry_sdk/integrations/sanic.py +++ b/sentry_sdk/integrations/sanic.py @@ -19,8 +19,6 @@ from sentry_sdk.tracing import TransactionSource from sentry_sdk.tracing_utils import has_span_streaming_enabled from sentry_sdk.utils import ( - CONTEXTVARS_ERROR_MESSAGE, - HAS_REAL_CONTEXTVARS, capture_internal_exceptions, ensure_integration_enabled, event_from_exception, @@ -80,14 +78,6 @@ def setup_once() -> None: SanicIntegration.version = parse_version(SANIC_VERSION) _check_minimum_version(SanicIntegration, SanicIntegration.version) - if not HAS_REAL_CONTEXTVARS: - # We better have contextvars or we're going to leak state between - # requests. - raise DidNotEnable( - "The sanic integration for Sentry requires Python 3.7+ " - " or the aiocontextvars package." + CONTEXTVARS_ERROR_MESSAGE - ) - if SANIC_VERSION.startswith("0.8."): # Sanic 0.8 and older creates a logger named "root" and puts a # stringified version of every exception in there (without exc_info), diff --git a/sentry_sdk/integrations/starlite.py b/sentry_sdk/integrations/starlite.py index 8963fc9e53..6bc26924ce 100644 --- a/sentry_sdk/integrations/starlite.py +++ b/sentry_sdk/integrations/starlite.py @@ -75,7 +75,6 @@ def __init__( ) -> None: super().__init__( app=app, - unsafe_context_data=False, transaction_style="endpoint", mechanism_type="asgi", span_origin=span_origin, diff --git a/sentry_sdk/integrations/tornado.py b/sentry_sdk/integrations/tornado.py index e71bf94d12..96c2298139 100644 --- a/sentry_sdk/integrations/tornado.py +++ b/sentry_sdk/integrations/tornado.py @@ -19,8 +19,6 @@ from sentry_sdk.tracing import TransactionSource from sentry_sdk.tracing_utils import has_span_streaming_enabled from sentry_sdk.utils import ( - CONTEXTVARS_ERROR_MESSAGE, - HAS_REAL_CONTEXTVARS, AnnotatedValue, capture_internal_exceptions, ensure_integration_enabled, @@ -54,14 +52,6 @@ class TornadoIntegration(Integration): def setup_once() -> None: _check_minimum_version(TornadoIntegration, TORNADO_VERSION) - if not HAS_REAL_CONTEXTVARS: - # Tornado is async. We better have contextvars or we're going to leak - # state between requests. - raise DidNotEnable( - "The tornado integration for Sentry requires Python 3.7+ or the aiocontextvars package" - + CONTEXTVARS_ERROR_MESSAGE - ) - ignore_logger("tornado.access") old_execute = RequestHandler._execute diff --git a/sentry_sdk/integrations/wsgi.py b/sentry_sdk/integrations/wsgi.py index b0156a9829..3d0cc4b876 100644 --- a/sentry_sdk/integrations/wsgi.py +++ b/sentry_sdk/integrations/wsgi.py @@ -1,4 +1,5 @@ import sys +from contextvars import ContextVar from functools import partial from typing import TYPE_CHECKING @@ -17,7 +18,6 @@ from sentry_sdk.tracing import Span, TransactionSource from sentry_sdk.tracing_utils import has_span_streaming_enabled from sentry_sdk.utils import ( - ContextVar, capture_internal_exceptions, event_from_exception, has_data_collection_enabled, @@ -56,7 +56,9 @@ def __call__( pass -_wsgi_middleware_applied = ContextVar("sentry_wsgi_middleware_applied") +_wsgi_middleware_applied: "ContextVar[bool]" = ContextVar( + "sentry_wsgi_middleware_applied" +) _DEFAULT_TRANSACTION_NAME = "generic WSGI request" diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 09e8876d6d..bb1fef7ae3 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -4,6 +4,7 @@ import warnings from collections import deque from contextlib import contextmanager +from contextvars import ContextVar from copy import copy, deepcopy from datetime import datetime, timezone from enum import Enum @@ -49,7 +50,6 @@ is_ignored_span, ) from sentry_sdk.utils import ( - ContextVar, capture_internal_exception, capture_internal_exceptions, datetime_from_isoformat, @@ -118,11 +118,15 @@ # This is used to isolate data for different requests or users. # The isolation scope is usually created by integrations, but may also # be created manually -_isolation_scope = ContextVar("isolation_scope", default=None) +_isolation_scope: "ContextVar[Optional[Scope]]" = ContextVar( + "isolation_scope", default=None +) # Holds data for the active span. # This can be used to manually add additional data to a span. -_current_scope = ContextVar("current_scope", default=None) +_current_scope: "ContextVar[Optional[Scope]]" = ContextVar( + "current_scope", default=None +) global_event_processors: "List[EventProcessor]" = [] diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index a6ece4faf1..ec32ee17b3 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -5,7 +5,6 @@ import logging import math import os -import random import re import subprocess import sys @@ -13,6 +12,7 @@ import time from collections import namedtuple from contextlib import contextmanager +from contextvars import ContextVar from datetime import datetime, timezone from decimal import Decimal from functools import partial, partialmethod, wraps @@ -1317,133 +1317,6 @@ def parse_version(version: str) -> "Optional[Tuple[int, ...]]": return release_tuple -def _is_contextvars_broken() -> bool: - """ - Returns whether gevent/eventlet have patched the stdlib in a way where thread locals are now more "correct" than contextvars. - """ - try: - import gevent - from gevent.monkey import is_object_patched - - # Get the MAJOR and MINOR version numbers of Gevent - version_tuple = tuple( - [int(part) for part in re.split(r"a|b|rc|\.", gevent.__version__)[:2]] - ) - if is_object_patched("threading", "local"): - # Gevent 20.9.0 depends on Greenlet 0.4.17 which natively handles switching - # context vars when greenlets are switched, so, Gevent 20.9.0+ is all fine. - # Ref: https://github.com/gevent/gevent/blob/83c9e2ae5b0834b8f84233760aabe82c3ba065b4/src/gevent/monkey.py#L604-L609 - # Gevent 20.5, that doesn't depend on Greenlet 0.4.17 with native support - # for contextvars, is able to patch both thread locals and contextvars, in - # that case, check if contextvars are effectively patched. - if ( - # Gevent 20.9.0+ - (sys.version_info >= (3, 7) and version_tuple >= (20, 9)) - # Gevent 20.5.0+ or Python < 3.7 - or (is_object_patched("contextvars", "ContextVar")) - ): - return False - - return True - except ImportError: - pass - - try: - import greenlet - from eventlet.patcher import is_monkey_patched # type: ignore - - greenlet_version = parse_version(greenlet.__version__) - - if greenlet_version is None: - logger.error( - "Internal error in Sentry SDK: Could not parse Greenlet version from greenlet.__version__." - ) - return False - - if is_monkey_patched("thread") and greenlet_version < (0, 5): - return True - except ImportError: - pass - - return False - - -def _make_threadlocal_contextvars(local: type) -> type: - class ContextVar: - # Super-limited impl of ContextVar - - def __init__(self, name: str, default: "Any" = None) -> None: - self._name = name - self._default = default - self._local = local() - self._original_local = local() - - def get(self, default: "Any" = None) -> "Any": - return getattr(self._local, "value", default or self._default) - - def set(self, value: "Any") -> "Any": - token = str(random.getrandbits(64)) - original_value = self.get() - setattr(self._original_local, token, original_value) - self._local.value = value - return token - - def reset(self, token: "Any") -> None: - self._local.value = getattr(self._original_local, token) - # delete the original value (this way it works in Python 3.6+) - del self._original_local.__dict__[token] - - return ContextVar - - -def _get_contextvars() -> "Tuple[bool, type]": - """ - Figure out the "right" contextvars installation to use. Returns a - `contextvars.ContextVar`-like class with a limited API. - - See https://docs.sentry.io/platforms/python/contextvars/ for more information. - """ - if not _is_contextvars_broken(): - # aiocontextvars is a PyPI package that ensures that the contextvars - # backport (also a PyPI package) works with asyncio under Python 3.6 - # - # Import it if available. - if sys.version_info < (3, 7): - # `aiocontextvars` is absolutely required for functional - # contextvars on Python 3.6. - try: - from aiocontextvars import ContextVar - - return True, ContextVar - except ImportError: - pass - else: - # On Python 3.7 contextvars are functional. - try: - from contextvars import ContextVar - - return True, ContextVar - except ImportError: - pass - - # Fall back to basic thread-local usage. - - from threading import local - - return False, _make_threadlocal_contextvars(local) - - -HAS_REAL_CONTEXTVARS, ContextVar = _get_contextvars() - -CONTEXTVARS_ERROR_MESSAGE = """ - -With asyncio/ASGI applications, the Sentry SDK requires a functional -installation of `contextvars` to avoid leaking scope/context data across -requests. - -Please refer to https://docs.sentry.io/platforms/python/contextvars/ for more information. -""" - _is_sentry_internal_task = ContextVar("is_sentry_internal_task", default=False) # These exceptions won't set the span status to error if they occur. Use @@ -1501,7 +1374,7 @@ def transaction_from_function(func: "Callable[..., Any]") -> "Optional[str]": return qualname_from_function(func) -disable_capture_event = ContextVar("disable_capture_event") +disable_capture_event: "ContextVar[bool]" = ContextVar("disable_capture_event") class ServerlessTimeoutWarning(Exception): # noqa: N818 diff --git a/tests/utils/test_contextvars.py b/tests/utils/test_contextvars.py index 50881314c1..5acfb5b15c 100644 --- a/tests/utils/test_contextvars.py +++ b/tests/utils/test_contextvars.py @@ -1,19 +1,14 @@ import random import time -from unittest import mock +from contextvars import ContextVar import pytest -def _run_contextvar_threaded_test(): +@pytest.mark.forked +def test_leaks(maybe_monkeypatched_threading): import threading - # Need to explicitly call _get_contextvars because the SDK has already - # decided upon gevent on import. - from sentry_sdk import utils - - _, ContextVar = utils._get_contextvars() # noqa: N806 - ts = [] var = ContextVar("test_contextvar_leaks") @@ -39,14 +34,3 @@ def run(): t.join() assert len(success) == 20 - - -@pytest.mark.forked -def test_leaks(maybe_monkeypatched_threading): - _run_contextvar_threaded_test() - - -@pytest.mark.forked -@mock.patch("sentry_sdk.utils._is_contextvars_broken", return_value=True) -def test_leaks_when_is_contextvars_broken_is_false(maybe_monkeypatched_threading): - _run_contextvar_threaded_test() From ca3ae7ada9f514b4f26dfa47c098f38d4c89708f Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 29 Jul 2026 12:00:14 +0200 Subject: [PATCH 2/6] migration guide --- MIGRATION_GUIDE.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index d38654dcc9..0e33b17021 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -9,6 +9,9 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh ## Removed +- Support for gevent versions below 20.9 has been removed. +- Support for greenlet versions below 0.5 has been removed. + ## Deprecated From 925575d614a00195c170a2cd8bc4412ac08f0ef6 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 29 Jul 2026 13:11:08 +0200 Subject: [PATCH 3/6] . --- sentry_sdk/_init_implementation.py | 6 ++--- tests/utils/test_contextvars.py | 36 ------------------------------ 2 files changed, 2 insertions(+), 40 deletions(-) delete mode 100644 tests/utils/test_contextvars.py diff --git a/sentry_sdk/_init_implementation.py b/sentry_sdk/_init_implementation.py index f889702512..4eb6de809a 100644 --- a/sentry_sdk/_init_implementation.py +++ b/sentry_sdk/_init_implementation.py @@ -1,7 +1,9 @@ +import re import warnings from typing import TYPE_CHECKING import sentry_sdk +from sentry_sdk.utils import logger, parse_version if TYPE_CHECKING: from typing import Any, ContextManager, Optional @@ -42,10 +44,6 @@ def __exit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None: def _check_version_deprecations() -> None: - import re - - from sentry_sdk.utils import logger, parse_version - try: import gevent diff --git a/tests/utils/test_contextvars.py b/tests/utils/test_contextvars.py deleted file mode 100644 index 5acfb5b15c..0000000000 --- a/tests/utils/test_contextvars.py +++ /dev/null @@ -1,36 +0,0 @@ -import random -import time -from contextvars import ContextVar - -import pytest - - -@pytest.mark.forked -def test_leaks(maybe_monkeypatched_threading): - import threading - - ts = [] - - var = ContextVar("test_contextvar_leaks") - - success = [] - - def run(): - value = int(random.random() * 1000) - var.set(value) - - for _ in range(100): - time.sleep(0) - assert var.get(None) == value - - success.append(1) - - for _ in range(20): - t = threading.Thread(target=run) - t.start() - ts.append(t) - - for t in ts: - t.join() - - assert len(success) == 20 From 7d977c67f10ea84eedab5d6e02bd1324ceaaea0f Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 29 Jul 2026 13:13:41 +0200 Subject: [PATCH 4/6] this is handled --- sentry_sdk/scope.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index bb1fef7ae3..071ad35f5b 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -471,8 +471,9 @@ def get_client(cls) -> "sentry_sdk.client.BaseClient": If no client is available a :py:class:`sentry_sdk.client.NonRecordingClient` is returned. """ current_scope = _current_scope.get() + try: - client = current_scope.client + client = current_scope.client # type: ignore[union-attr] except AttributeError: client = None @@ -481,7 +482,7 @@ def get_client(cls) -> "sentry_sdk.client.BaseClient": isolation_scope = _isolation_scope.get() try: - client = isolation_scope.client + client = isolation_scope.client # type: ignore[union-attr] except AttributeError: client = None From 7d7ac5333b56f2dcb31363dbfecf4b27c2e2ed94 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 29 Jul 2026 13:44:09 +0200 Subject: [PATCH 5/6] fix greenlet version --- MIGRATION_GUIDE.md | 4 ++-- sentry_sdk/_init_implementation.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 0e33b17021..3d448a481f 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -9,8 +9,8 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh ## Removed -- Support for gevent versions below 20.9 has been removed. -- Support for greenlet versions below 0.5 has been removed. +- Dropped support for gevent versions below 20.9. +- Dropped support for greenlet versions below 0.4.17. ## Deprecated diff --git a/sentry_sdk/_init_implementation.py b/sentry_sdk/_init_implementation.py index 4eb6de809a..4fc9368628 100644 --- a/sentry_sdk/_init_implementation.py +++ b/sentry_sdk/_init_implementation.py @@ -62,9 +62,9 @@ def _check_version_deprecations() -> None: import greenlet greenlet_version = parse_version(greenlet.__version__) - if greenlet_version is not None and greenlet_version < (0, 5): + if greenlet_version is not None and greenlet_version < (0, 4, 17): logger.warning( - "sentry-sdk 3.x supports greenlet 0.5 or newer. " + "sentry-sdk 3.x supports greenlet 0.4.17 or newer. " "Please upgrade greenlet or downgrade to sentry-sdk 2.x." ) except ImportError: From bc0833fd2e900eba6621ef15b7e83b178728591e Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 29 Jul 2026 13:50:46 +0200 Subject: [PATCH 6/6] . --- sentry_sdk/_init_implementation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/_init_implementation.py b/sentry_sdk/_init_implementation.py index 4fc9368628..3051ae010b 100644 --- a/sentry_sdk/_init_implementation.py +++ b/sentry_sdk/_init_implementation.py @@ -55,7 +55,7 @@ def _check_version_deprecations() -> None: "sentry-sdk 3.x supports gevent 20.9.0 or newer. " "Please upgrade gevent or downgrade to sentry-sdk 2.x." ) - except ImportError: + except Exception: pass try: @@ -67,7 +67,7 @@ def _check_version_deprecations() -> None: "sentry-sdk 3.x supports greenlet 0.4.17 or newer. " "Please upgrade greenlet or downgrade to sentry-sdk 2.x." ) - except ImportError: + except Exception: pass