Skip to content

Commit fb1b746

Browse files
authored
fix(django): fix Django ASGI integration on Python 3.12 (#3027)
1 parent eaad88a commit fb1b746

File tree

2 files changed

+89
-5
lines changed

2 files changed

+89
-5
lines changed

sentry_sdk/integrations/django/asgi.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import asyncio
1010
import functools
11+
import inspect
1112

1213
from django.core.handlers.wsgi import WSGIRequest
1314

@@ -25,14 +26,31 @@
2526

2627

2728
if TYPE_CHECKING:
28-
from collections.abc import Callable
29-
from typing import Any, Union
29+
from typing import Any, Callable, Union, TypeVar
3030

3131
from django.core.handlers.asgi import ASGIRequest
3232
from django.http.response import HttpResponse
3333

3434
from sentry_sdk._types import Event, EventProcessor
3535

36+
_F = TypeVar("_F", bound=Callable[..., Any])
37+
38+
39+
# Python 3.12 deprecates asyncio.iscoroutinefunction() as an alias for
40+
# inspect.iscoroutinefunction(), whilst also removing the _is_coroutine marker.
41+
# The latter is replaced with the inspect.markcoroutinefunction decorator.
42+
# Until 3.12 is the minimum supported Python version, provide a shim.
43+
# This was copied from https://github.com/django/asgiref/blob/main/asgiref/sync.py
44+
if hasattr(inspect, "markcoroutinefunction"):
45+
iscoroutinefunction = inspect.iscoroutinefunction
46+
markcoroutinefunction = inspect.markcoroutinefunction
47+
else:
48+
iscoroutinefunction = asyncio.iscoroutinefunction # type: ignore[assignment]
49+
50+
def markcoroutinefunction(func: "_F") -> "_F":
51+
func._is_coroutine = asyncio.coroutines._is_coroutine # type: ignore
52+
return func
53+
3654

3755
def _make_asgi_request_event_processor(request):
3856
# type: (ASGIRequest) -> EventProcessor
@@ -181,16 +199,16 @@ def _async_check(self):
181199
a thread is not consumed during a whole request.
182200
Taken from django.utils.deprecation::MiddlewareMixin._async_check
183201
"""
184-
if asyncio.iscoroutinefunction(self.get_response):
185-
self._is_coroutine = asyncio.coroutines._is_coroutine # type: ignore
202+
if iscoroutinefunction(self.get_response):
203+
markcoroutinefunction(self)
186204

187205
def async_route_check(self):
188206
# type: () -> bool
189207
"""
190208
Function that checks if we are in async mode,
191209
and if we are forwards the handling of requests to __acall__
192210
"""
193-
return asyncio.iscoroutinefunction(self.get_response)
211+
return iscoroutinefunction(self.get_response)
194212

195213
async def __acall__(self, *args, **kwargs):
196214
# type: (*Any, **Any) -> Any

tests/integrations/django/asgi/test_asgi.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import base64
2+
import sys
23
import json
4+
import inspect
5+
import asyncio
36
import os
47
from unittest import mock
58

@@ -8,6 +11,7 @@
811
from channels.testing import HttpCommunicator
912
from sentry_sdk import capture_message
1013
from sentry_sdk.integrations.django import DjangoIntegration
14+
from sentry_sdk.integrations.django.asgi import _asgi_middleware_mixin_factory
1115
from tests.integrations.django.myapp.asgi import channels_application
1216

1317
try:
@@ -526,3 +530,65 @@ async def test_asgi_request_body(
526530
assert event["request"]["data"] == expected_data
527531
else:
528532
assert "data" not in event["request"]
533+
534+
535+
@pytest.mark.asyncio
536+
@pytest.mark.skipif(
537+
sys.version_info >= (3, 12),
538+
reason=(
539+
"asyncio.iscoroutinefunction has been replaced in 3.12 by inspect.iscoroutinefunction"
540+
),
541+
)
542+
async def test_asgi_mixin_iscoroutinefunction_before_3_12():
543+
sentry_asgi_mixin = _asgi_middleware_mixin_factory(lambda: None)
544+
545+
async def get_response(): ...
546+
547+
instance = sentry_asgi_mixin(get_response)
548+
assert asyncio.iscoroutinefunction(instance)
549+
550+
551+
@pytest.mark.skipif(
552+
sys.version_info >= (3, 12),
553+
reason=(
554+
"asyncio.iscoroutinefunction has been replaced in 3.12 by inspect.iscoroutinefunction"
555+
),
556+
)
557+
def test_asgi_mixin_iscoroutinefunction_when_not_async_before_3_12():
558+
sentry_asgi_mixin = _asgi_middleware_mixin_factory(lambda: None)
559+
560+
def get_response(): ...
561+
562+
instance = sentry_asgi_mixin(get_response)
563+
assert not asyncio.iscoroutinefunction(instance)
564+
565+
566+
@pytest.mark.asyncio
567+
@pytest.mark.skipif(
568+
sys.version_info < (3, 12),
569+
reason=(
570+
"asyncio.iscoroutinefunction has been replaced in 3.12 by inspect.iscoroutinefunction"
571+
),
572+
)
573+
async def test_asgi_mixin_iscoroutinefunction_after_3_12():
574+
sentry_asgi_mixin = _asgi_middleware_mixin_factory(lambda: None)
575+
576+
async def get_response(): ...
577+
578+
instance = sentry_asgi_mixin(get_response)
579+
assert inspect.iscoroutinefunction(instance)
580+
581+
582+
@pytest.mark.skipif(
583+
sys.version_info < (3, 12),
584+
reason=(
585+
"asyncio.iscoroutinefunction has been replaced in 3.12 by inspect.iscoroutinefunction"
586+
),
587+
)
588+
def test_asgi_mixin_iscoroutinefunction_when_not_async_after_3_12():
589+
sentry_asgi_mixin = _asgi_middleware_mixin_factory(lambda: None)
590+
591+
def get_response(): ...
592+
593+
instance = sentry_asgi_mixin(get_response)
594+
assert not inspect.iscoroutinefunction(instance)

0 commit comments

Comments
 (0)