Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
42 changes: 41 additions & 1 deletion sentry_sdk/_compat.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import sys
import asyncio
import inspect

from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any, Callable

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

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

# Public shim symbols with precise types so mypy accepts branch assignments
iscoroutinefunction: "Callable[[Any], bool]"
markcoroutinefunction: "Callable[[ _F ], _F]"


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

Copy link
Contributor

Choose a reason for hiding this comment

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

Switching asyncio.iscoroutinefunction for inspect.iscoroutinefunction is tricky because of historical incompatibility. Since we support Python versions 3.6 and up we have to care about this unfortunately.

We should use the most conservative cutoff to not break any users. So while CPython deprecated asyncio.iscoroutinefunction in version 3.12, we should probably mirror Starlette who use 3.13 because of a bug in standard libraries which have not been backported. See Kludex/starlette#2983

Also, since we vendor _is_async_callable, the change below would keep the vendor up to date. We can therefore be more confident that we won't run into edge cases, since users of starlette would have run into them already.

Suggested change
PY313 = sys.version_info[0] == 3 and sys.version_info[1] >= 13
iscoroutinefunction = inspect.iscoroutinefunction if PY313 else asyncio.iscoroutinefunction


# 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

def markcoroutinefunction(func):
# type: (_F) -> _F
# Prior to Python 3.12, asyncio exposed a private `_is_coroutine`
# marker used by asyncio.iscoroutinefunction(). This attribute was
# removed in Python 3.11. If it's not available, fall back to a no-op,
# which preserves behavior of inspect.iscoroutinefunction for our
# supported versions while avoiding AttributeError.
try:
marker = getattr(asyncio.coroutines, "_is_coroutine")
except Exception:
# No marker available on this Python version; return function as-is.
return func

try: # pragma: no cover - defensive
func._is_coroutine = marker # type: ignore[attr-defined]
except Exception:
# If assignment fails for any reason, leave func unchanged.
pass
return func


def with_metaclass(meta, *bases):
# type: (Any, *Any) -> Any
class MetaClass(type):
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)
Copy link
Contributor

Choose a reason for hiding this comment

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

No action required, just documenting.

This logic is vendored from uvicorn and they recently replaced asyncio.iscoroutinefunction with inspect.iscoroutinefunction. See Kludex/uvicorn#2659

Since we will check for Python 3.13 and up and uvicorn seems to be happy using inspect on 3.9 and up, we can rely on uvicorn having battle testing this change as well.

else:
call = getattr(app, "__call__", None) # noqa
return asyncio.iscoroutinefunction(call)
return iscoroutinefunction(call)


class SentryAsgiMiddleware:
Expand Down
17 changes: 1 addition & 16 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,22 +36,6 @@
_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):
# type: (ASGIRequest) -> EventProcessor
def asgi_request_event_processor(event, hint):
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
5 changes: 2 additions & 3 deletions 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,9 +114,7 @@ def _sentry_route(*args, **kwargs):
def decorator(old_func):
# type: (Any) -> Any

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

@wraps(old_func)
@ensure_integration_enabled(QuartIntegration, 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
27 changes: 14 additions & 13 deletions tests/integrations/httpx/test_httpx.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os
import datetime
import asyncio
import inspect
from unittest import mock

import httpx
Expand Down Expand Up @@ -32,7 +33,7 @@ def before_breadcrumb(crumb, hint):
with start_transaction():
events = capture_events()

if asyncio.iscoroutinefunction(httpx_client.get):
if inspect.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 inspect.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 inspect.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 inspect.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 inspect.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 inspect.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 inspect.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 inspect.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 inspect.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 inspect.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 inspect.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 inspect.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 inspect.iscoroutinefunction(httpx_client.get):
asyncio.get_event_loop().run_until_complete(httpx_client.get(url))
else:
httpx_client.get(url)
Expand Down
Loading