Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 21 additions & 0 deletions sentry_sdk/_compat.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import sys
import asyncio
import inspect

from typing import TYPE_CHECKING

if TYPE_CHECKING:
from typing import Any
from typing import TypeVar
from typing import Callable

T = TypeVar("T")
_F = TypeVar("_F", bound=Callable[..., Any])


PY37 = sys.version_info[0] == 3 and sys.version_info[1] >= 7
Expand All @@ -15,6 +19,23 @@
PY311 = sys.version_info[0] == 3 and sys.version_info[1] >= 11


# Python 3.12 deprecates asyncio.iscoroutinefunction() as an alias for
# inspect.iscoroutinefunction(), whilst also removing the _is_coroutine marker.
# The latter is replaced with the inspect.markcoroutinefunction decorator.
# Until 3.12 is the minimum supported Python version, provide a shim.
# This was adapted from https://github.com/django/asgiref/blob/main/asgiref/sync.py
if hasattr(inspect, "markcoroutinefunction"):
iscoroutinefunction = inspect.iscoroutinefunction
markcoroutinefunction = inspect.markcoroutinefunction
else:
iscoroutinefunction = asyncio.iscoroutinefunction # type: ignore[assignment]

def markcoroutinefunction(func):
# type: (_F) -> _F
func._is_coroutine = asyncio.coroutines._is_coroutine # type: ignore
return func
Comment on lines +31 to +36

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential bug: The fallback for markcoroutinefunction uses asyncio.coroutines._is_coroutine, which was removed in Python 3.11, causing an AttributeError.
  • Description: The compatibility shim for markcoroutinefunction provides a fallback for Python versions older than 3.12. This fallback incorrectly assumes asyncio.coroutines._is_coroutine exists. However, this attribute was removed in Python 3.11. When the SDK is used with a Django ASGI application on Python 3.11, the fallback logic is triggered, leading to an AttributeError when markcoroutinefunction is called in sentry_sdk/integrations/django/asgi.py.

  • Suggested fix: The fallback implementation for markcoroutinefunction needs to be updated to not use asyncio.coroutines._is_coroutine. A different implementation that is compatible with Python versions prior to 3.12, including 3.11, should be used.
    severity: 0.85, confidence: 0.95

Did we get this right? 👍 / 👎 to inform future reviews.



def with_metaclass(meta, *bases):
# type: (Any, *Any) -> Any
class MetaClass(type):
Expand Down
3 changes: 2 additions & 1 deletion sentry_sdk/ai/monitoring.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import inspect
from functools import wraps

from sentry_sdk._compat import iscoroutinefunction
from sentry_sdk.consts import SPANDATA
import sentry_sdk.utils
from sentry_sdk import start_span
Expand Down Expand Up @@ -89,7 +90,7 @@ async def async_wrapped(*args, **kwargs):
_ai_pipeline_name.set(None)
return res

if inspect.iscoroutinefunction(f):
if iscoroutinefunction(f):
return wraps(f)(async_wrapped) # type: ignore
else:
return wraps(f)(sync_wrapped) # type: ignore
Expand Down
5 changes: 3 additions & 2 deletions sentry_sdk/integrations/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from functools import partial

import sentry_sdk
from sentry_sdk._compat import iscoroutinefunction
from sentry_sdk.api import continue_trace
from sentry_sdk.consts import OP
from sentry_sdk.integrations._asgi_common import (
Expand Down Expand Up @@ -76,10 +77,10 @@ def _looks_like_asgi3(app):
if inspect.isclass(app):
return hasattr(app, "__await__")
elif inspect.isfunction(app):
return asyncio.iscoroutinefunction(app)
return iscoroutinefunction(app)
else:
call = getattr(app, "__call__", None) # noqa
return asyncio.iscoroutinefunction(call)
return iscoroutinefunction(call)


class SentryAsgiMiddleware:
Expand Down
16 changes: 2 additions & 14 deletions sentry_sdk/integrations/django/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from django.core.handlers.wsgi import WSGIRequest

import sentry_sdk
from sentry_sdk._compat import iscoroutinefunction, markcoroutinefunction
from sentry_sdk.consts import OP

from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
Expand All @@ -35,20 +36,7 @@
_F = TypeVar("_F", bound=Callable[..., Any])


# Python 3.12 deprecates asyncio.iscoroutinefunction() as an alias for
# inspect.iscoroutinefunction(), whilst also removing the _is_coroutine marker.
# The latter is replaced with the inspect.markcoroutinefunction decorator.
# Until 3.12 is the minimum supported Python version, provide a shim.
# This was copied from https://github.com/django/asgiref/blob/main/asgiref/sync.py
if hasattr(inspect, "markcoroutinefunction"):
iscoroutinefunction = inspect.iscoroutinefunction
markcoroutinefunction = inspect.markcoroutinefunction
else:
iscoroutinefunction = asyncio.iscoroutinefunction # type: ignore[assignment]

def markcoroutinefunction(func: "_F") -> "_F":
func._is_coroutine = asyncio.coroutines._is_coroutine # type: ignore
return func



def _make_asgi_request_event_processor(request):
Expand Down
3 changes: 2 additions & 1 deletion sentry_sdk/integrations/fastapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from functools import wraps

import sentry_sdk
from sentry_sdk._compat import iscoroutinefunction
from sentry_sdk.integrations import DidNotEnable
from sentry_sdk.scope import should_send_default_pii
from sentry_sdk.tracing import SOURCE_FOR_STYLE, TransactionSource
Expand Down Expand Up @@ -75,7 +76,7 @@ def _sentry_get_request_handler(*args, **kwargs):
if (
dependant
and dependant.call is not None
and not asyncio.iscoroutinefunction(dependant.call)
and not iscoroutinefunction(dependant.call)
):
old_call = dependant.call

Expand Down
3 changes: 2 additions & 1 deletion sentry_sdk/integrations/google_genai/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
)

import sentry_sdk
from sentry_sdk._compat import iscoroutinefunction
from sentry_sdk.ai.utils import set_data_normalized
from sentry_sdk.consts import OP, SPANDATA
from sentry_sdk.scope import should_send_default_pii
Expand Down Expand Up @@ -318,7 +319,7 @@ def wrapped_tool(tool):
tool_name = getattr(tool, "__name__", "unknown")
tool_doc = tool.__doc__

if inspect.iscoroutinefunction(tool):
if iscoroutinefunction(tool):
# Async function
@wraps(tool)
async def async_wrapped(*args, **kwargs):
Expand Down
3 changes: 2 additions & 1 deletion sentry_sdk/integrations/quart.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from functools import wraps

import sentry_sdk
from sentry_sdk._compat import iscoroutinefunction
from sentry_sdk.integrations import DidNotEnable, Integration
from sentry_sdk.integrations._wsgi_common import _filter_headers
from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
Expand Down Expand Up @@ -113,7 +114,7 @@ def _sentry_route(*args, **kwargs):
def decorator(old_func):
# type: (Any) -> Any

if inspect.isfunction(old_func) and not asyncio.iscoroutinefunction(
if inspect.isfunction(old_func) and not iscoroutinefunction(
old_func
):

Expand Down
5 changes: 3 additions & 2 deletions sentry_sdk/integrations/starlette.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from json import JSONDecodeError

import sentry_sdk
from sentry_sdk._compat import iscoroutinefunction
from sentry_sdk.consts import OP
from sentry_sdk.integrations import (
DidNotEnable,
Expand Down Expand Up @@ -415,8 +416,8 @@ def _is_async_callable(obj):
while isinstance(obj, functools.partial):
obj = obj.func

return asyncio.iscoroutinefunction(obj) or (
callable(obj) and asyncio.iscoroutinefunction(obj.__call__)
return iscoroutinefunction(obj) or (
callable(obj) and iscoroutinefunction(obj.__call__)
)


Expand Down
3 changes: 2 additions & 1 deletion sentry_sdk/tracing_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import uuid

import sentry_sdk
from sentry_sdk._compat import iscoroutinefunction
from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS, SPANTEMPLATE
from sentry_sdk.utils import (
capture_internal_exceptions,
Expand Down Expand Up @@ -912,7 +913,7 @@ def sync_wrapper(*args, **kwargs):
except Exception:
pass

if inspect.iscoroutinefunction(f):
if iscoroutinefunction(f):
return async_wrapper
else:
return sync_wrapper
Expand Down
27 changes: 14 additions & 13 deletions tests/integrations/httpx/test_httpx.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import sentry_sdk
from sentry_sdk import capture_message, start_transaction
from sentry_sdk._compat import iscoroutinefunction
from sentry_sdk.consts import MATCH_ALL, SPANDATA
from sentry_sdk.integrations.httpx import HttpxIntegration
from tests.conftest import ApproxDict
Expand All @@ -32,7 +33,7 @@ def before_breadcrumb(crumb, hint):
with start_transaction():
events = capture_events()

if asyncio.iscoroutinefunction(httpx_client.get):
if iscoroutinefunction(httpx_client.get):
response = asyncio.get_event_loop().run_until_complete(
httpx_client.get(url)
)
Expand Down Expand Up @@ -86,7 +87,7 @@ def test_crumb_capture_client_error(
with start_transaction():
events = capture_events()

if asyncio.iscoroutinefunction(httpx_client.get):
if iscoroutinefunction(httpx_client.get):
response = asyncio.get_event_loop().run_until_complete(
httpx_client.get(url)
)
Expand Down Expand Up @@ -137,7 +138,7 @@ def test_outgoing_trace_headers(sentry_init, httpx_client, httpx_mock):
op="greeting.sniff",
trace_id="01234567890123456789012345678901",
) as transaction:
if asyncio.iscoroutinefunction(httpx_client.get):
if iscoroutinefunction(httpx_client.get):
response = asyncio.get_event_loop().run_until_complete(
httpx_client.get(url)
)
Expand Down Expand Up @@ -180,7 +181,7 @@ def test_outgoing_trace_headers_append_to_baggage(
op="greeting.sniff",
trace_id="01234567890123456789012345678901",
) as transaction:
if asyncio.iscoroutinefunction(httpx_client.get):
if iscoroutinefunction(httpx_client.get):
response = asyncio.get_event_loop().run_until_complete(
httpx_client.get(url, headers={"baGGage": "custom=data"})
)
Expand Down Expand Up @@ -333,7 +334,7 @@ def test_option_trace_propagation_targets(

# Must be in a transaction to propagate headers
with sentry_sdk.start_transaction():
if asyncio.iscoroutinefunction(httpx_client.get):
if iscoroutinefunction(httpx_client.get):
asyncio.get_event_loop().run_until_complete(httpx_client.get(url))
else:
httpx_client.get(url)
Expand Down Expand Up @@ -420,7 +421,7 @@ def test_request_source_disabled(
url = "http://example.com/"

with start_transaction(name="test_transaction"):
if asyncio.iscoroutinefunction(httpx_client.get):
if iscoroutinefunction(httpx_client.get):
asyncio.get_event_loop().run_until_complete(httpx_client.get(url))
else:
httpx_client.get(url)
Expand Down Expand Up @@ -457,7 +458,7 @@ def test_request_source_enabled(sentry_init, capture_events, httpx_client, httpx
url = "http://example.com/"

with start_transaction(name="test_transaction"):
if asyncio.iscoroutinefunction(httpx_client.get):
if iscoroutinefunction(httpx_client.get):
asyncio.get_event_loop().run_until_complete(httpx_client.get(url))
else:
httpx_client.get(url)
Expand Down Expand Up @@ -494,7 +495,7 @@ def test_request_source(sentry_init, capture_events, httpx_client, httpx_mock):
url = "http://example.com/"

with start_transaction(name="test_transaction"):
if asyncio.iscoroutinefunction(httpx_client.get):
if iscoroutinefunction(httpx_client.get):
asyncio.get_event_loop().run_until_complete(httpx_client.get(url))
else:
httpx_client.get(url)
Expand Down Expand Up @@ -547,7 +548,7 @@ def test_request_source_with_module_in_search_path(
url = "http://example.com/"

with start_transaction(name="test_transaction"):
if asyncio.iscoroutinefunction(httpx_client.get):
if iscoroutinefunction(httpx_client.get):
from httpx_helpers.helpers import async_get_request_with_client

asyncio.get_event_loop().run_until_complete(
Expand Down Expand Up @@ -578,7 +579,7 @@ def test_request_source_with_module_in_search_path(
is_relative_path = data.get(SPANDATA.CODE_FILEPATH)[0] != os.sep
assert is_relative_path

if asyncio.iscoroutinefunction(httpx_client.get):
if iscoroutinefunction(httpx_client.get):
assert data.get(SPANDATA.CODE_FUNCTION) == "async_get_request_with_client"
else:
assert data.get(SPANDATA.CODE_FUNCTION) == "get_request_with_client"
Expand Down Expand Up @@ -618,7 +619,7 @@ def fake_start_span(*args, **kwargs):
"sentry_sdk.integrations.httpx.start_span",
fake_start_span,
):
if asyncio.iscoroutinefunction(httpx_client.get):
if iscoroutinefunction(httpx_client.get):
asyncio.get_event_loop().run_until_complete(httpx_client.get(url))
else:
httpx_client.get(url)
Expand Down Expand Up @@ -670,7 +671,7 @@ def fake_start_span(*args, **kwargs):
"sentry_sdk.integrations.httpx.start_span",
fake_start_span,
):
if asyncio.iscoroutinefunction(httpx_client.get):
if iscoroutinefunction(httpx_client.get):
asyncio.get_event_loop().run_until_complete(httpx_client.get(url))
else:
httpx_client.get(url)
Expand Down Expand Up @@ -720,7 +721,7 @@ def test_span_origin(sentry_init, capture_events, httpx_client, httpx_mock):
url = "http://example.com/"

with start_transaction(name="test_transaction"):
if asyncio.iscoroutinefunction(httpx_client.get):
if iscoroutinefunction(httpx_client.get):
asyncio.get_event_loop().run_until_complete(httpx_client.get(url))
else:
httpx_client.get(url)
Expand Down