Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: fix Django ASGI integration on Python 3.12 #3027

Merged
merged 3 commits into from
Apr 30, 2024
Merged
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
28 changes: 23 additions & 5 deletions sentry_sdk/integrations/django/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import asyncio
import functools
import inspect

from django.core.handlers.wsgi import WSGIRequest

Expand All @@ -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
Expand Down Expand Up @@ -181,16 +199,16 @@ 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
"""
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
Expand Down
66 changes: 66 additions & 0 deletions tests/integrations/django/asgi/test_asgi.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import base64
import sys
import json
import inspect
import asyncio
import os
from unittest import mock

Expand All @@ -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:
Expand Down Expand Up @@ -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)
Loading