Skip to content

Commit ca19d63

Browse files
feat: Preserve metadata on wrapped coroutines (#5105)
Copy metadata when wrapping coroutines in the patched `asyncio` task factory. Uses the `functools.update_wrapper()` function that is also used internally by `functools.wraps()`. Unlike when wrapping functions, less metadata is available on coroutines, so `_wrap_coroutine()` is equivalent to `functools.wraps()` but copies fewer attributes. As `functools.update_wrapper()` checks for the existence of properties first, copying metadata will not result in an `AttributeError` if metadata is not available. See https://docs.python.org/3/library/functools.html#functools.update_wrapper. The SDK no longer overwrites coroutine metadata included in destroyed errors as reported in #5072.
1 parent cf165e3 commit ca19d63

File tree

2 files changed

+26
-2
lines changed

2 files changed

+26
-2
lines changed

sentry_sdk/integrations/asyncio.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import sys
2+
import functools
23

34
import sentry_sdk
45
from sentry_sdk.consts import OP
@@ -14,11 +15,13 @@
1415
from typing import cast, TYPE_CHECKING
1516

1617
if TYPE_CHECKING:
17-
from typing import Any
18+
from typing import Any, Callable, TypeVar
1819
from collections.abc import Coroutine
1920

2021
from sentry_sdk._types import ExcInfo
2122

23+
T = TypeVar("T", bound=Callable[..., Any])
24+
2225

2326
def get_name(coro):
2427
# type: (Any) -> str
@@ -29,6 +32,17 @@ def get_name(coro):
2932
)
3033

3134

35+
def _wrap_coroutine(wrapped):
36+
# type: (Coroutine[Any, Any, Any]) -> Callable[[T], T]
37+
# Only __name__ and __qualname__ are copied from function to coroutine in CPython
38+
return functools.partial(
39+
functools.update_wrapper,
40+
wrapped=wrapped, # type: ignore
41+
assigned=("__name__", "__qualname__"),
42+
updated=(),
43+
)
44+
45+
3246
def patch_asyncio():
3347
# type: () -> None
3448
orig_task_factory = None
@@ -39,6 +53,7 @@ def patch_asyncio():
3953
def _sentry_task_factory(loop, coro, **kwargs):
4054
# type: (asyncio.AbstractEventLoop, Coroutine[Any, Any, Any], Any) -> asyncio.Future[Any]
4155

56+
@_wrap_coroutine(coro)
4257
async def _task_with_sentry_span_creation():
4358
# type: () -> Any
4459
result = None

tests/integrations/asyncio/test_asyncio.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,16 @@ async def test_create_task(
6767

6868
with sentry_sdk.start_transaction(name="test_transaction_for_create_task"):
6969
with sentry_sdk.start_span(op="root", name="not so important"):
70-
tasks = [asyncio.create_task(foo()), asyncio.create_task(bar())]
70+
foo_task = asyncio.create_task(foo())
71+
bar_task = asyncio.create_task(bar())
72+
73+
if hasattr(foo_task.get_coro(), "__name__"):
74+
assert foo_task.get_coro().__name__ == "foo"
75+
if hasattr(bar_task.get_coro(), "__name__"):
76+
assert bar_task.get_coro().__name__ == "bar"
77+
78+
tasks = [foo_task, bar_task]
79+
7180
await asyncio.wait(tasks, return_when=asyncio.FIRST_EXCEPTION)
7281

7382
sentry_sdk.flush()

0 commit comments

Comments
 (0)