diff --git a/sentry_sdk/integrations/django/asgi.py b/sentry_sdk/integrations/django/asgi.py index b52ca6dd33..e62ce681e7 100644 --- a/sentry_sdk/integrations/django/asgi.py +++ b/sentry_sdk/integrations/django/asgi.py @@ -8,6 +8,7 @@ import asyncio import functools +import inspect from django.core.handlers.wsgi import WSGIRequest @@ -25,14 +26,31 @@ if TYPE_CHECKING: - from collections.abc import Callable - from typing import Any, Union + from typing import Any, Callable, Union, TypeVar from django.core.handlers.asgi import ASGIRequest from django.http.response import HttpResponse from sentry_sdk._types import Event, EventProcessor + _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 @@ -181,8 +199,8 @@ def _async_check(self): a thread is not consumed during a whole request. Taken from django.utils.deprecation::MiddlewareMixin._async_check """ - if asyncio.iscoroutinefunction(self.get_response): - self._is_coroutine = asyncio.coroutines._is_coroutine # type: ignore + if iscoroutinefunction(self.get_response): + markcoroutinefunction(self) def async_route_check(self): # type: () -> bool @@ -190,7 +208,7 @@ def async_route_check(self): Function that checks if we are in async mode, and if we are forwards the handling of requests to __acall__ """ - return asyncio.iscoroutinefunction(self.get_response) + return iscoroutinefunction(self.get_response) async def __acall__(self, *args, **kwargs): # type: (*Any, **Any) -> Any diff --git a/tests/integrations/django/asgi/test_asgi.py b/tests/integrations/django/asgi/test_asgi.py index fd266c4fae..47e333cc37 100644 --- a/tests/integrations/django/asgi/test_asgi.py +++ b/tests/integrations/django/asgi/test_asgi.py @@ -1,5 +1,8 @@ import base64 +import sys import json +import inspect +import asyncio import os from unittest import mock @@ -8,6 +11,7 @@ from channels.testing import HttpCommunicator from sentry_sdk import capture_message from sentry_sdk.integrations.django import DjangoIntegration +from sentry_sdk.integrations.django.asgi import _asgi_middleware_mixin_factory from tests.integrations.django.myapp.asgi import channels_application try: @@ -526,3 +530,65 @@ async def test_asgi_request_body( assert event["request"]["data"] == expected_data else: assert "data" not in event["request"] + + +@pytest.mark.asyncio +@pytest.mark.skipif( + sys.version_info >= (3, 12), + reason=( + "asyncio.iscoroutinefunction has been replaced in 3.12 by inspect.iscoroutinefunction" + ), +) +async def test_asgi_mixin_iscoroutinefunction_before_3_12(): + sentry_asgi_mixin = _asgi_middleware_mixin_factory(lambda: None) + + async def get_response(): ... + + instance = sentry_asgi_mixin(get_response) + assert asyncio.iscoroutinefunction(instance) + + +@pytest.mark.skipif( + sys.version_info >= (3, 12), + reason=( + "asyncio.iscoroutinefunction has been replaced in 3.12 by inspect.iscoroutinefunction" + ), +) +def test_asgi_mixin_iscoroutinefunction_when_not_async_before_3_12(): + sentry_asgi_mixin = _asgi_middleware_mixin_factory(lambda: None) + + def get_response(): ... + + instance = sentry_asgi_mixin(get_response) + assert not asyncio.iscoroutinefunction(instance) + + +@pytest.mark.asyncio +@pytest.mark.skipif( + sys.version_info < (3, 12), + reason=( + "asyncio.iscoroutinefunction has been replaced in 3.12 by inspect.iscoroutinefunction" + ), +) +async def test_asgi_mixin_iscoroutinefunction_after_3_12(): + sentry_asgi_mixin = _asgi_middleware_mixin_factory(lambda: None) + + async def get_response(): ... + + instance = sentry_asgi_mixin(get_response) + assert inspect.iscoroutinefunction(instance) + + +@pytest.mark.skipif( + sys.version_info < (3, 12), + reason=( + "asyncio.iscoroutinefunction has been replaced in 3.12 by inspect.iscoroutinefunction" + ), +) +def test_asgi_mixin_iscoroutinefunction_when_not_async_after_3_12(): + sentry_asgi_mixin = _asgi_middleware_mixin_factory(lambda: None) + + def get_response(): ... + + instance = sentry_asgi_mixin(get_response) + assert not inspect.iscoroutinefunction(instance)