Skip to content

feat(celery): preserve original signature on apply_async wrapper for autospec compatibility#6400

Draft
JVenberg wants to merge 1 commit into
getsentry:masterfrom
JVenberg:DEV-142980-celery-autospec-signature
Draft

feat(celery): preserve original signature on apply_async wrapper for autospec compatibility#6400
JVenberg wants to merge 1 commit into
getsentry:masterfrom
JVenberg:DEV-142980-celery-autospec-signature

Conversation

@JVenberg
Copy link
Copy Markdown

Summary

_wrap_task_run replaces Task.apply_async (and Celery.send_task) with a wrapper declared as:

@wraps(f)
def apply_async(*args, **kwargs):
    ...

functools.wraps copies __wrapped__ onto the wrapper, so inspect.signature resolves through it. But other introspection tools that do not follow __wrapped__ (e.g. inspect.getcallargs, IDE call-arg analysis, and some autospec-style test doubles built on __code__ rather than signature) see the wrapper's bare (*args, **kwargs) and lose the original parameter names.

This change adds:

try:
    apply_async.__signature__ = inspect.signature(f)
except (TypeError, ValueError):
    pass

after the wrapper is defined, making the wrapper signature-transparent to all introspection paths.

Precedent

PR #3178 (fix @sentry_sdk.tracing.trace changing function signature, fixing #3177) applied this exact pattern to func_with_tracing in sentry_sdk/tracing_utils.py. The _wrap_task_run wrapper has the same shape; this PR brings it in line.

Reproduction

import inspect
from celery import Celery
import sentry_sdk
from sentry_sdk.integrations.celery import CeleryIntegration

sentry_sdk.init(integrations=[CeleryIntegration()])

celery = Celery("repro")

@celery.task
def my_task(x, y):
    return x + y

from celery.app.task import Task

# Before this PR: getcallargs sees the bare (*args, **kwargs):
#   {'args': (None, (1, 2), {}), 'kwargs': {}}
# After this PR: getcallargs binds parameter names from the wrapped function:
#   {'self': None, 'args': (1, 2), 'kwargs': {}, 'task_id': None, ...}
print(inspect.getcallargs(Task.apply_async, None, (1, 2), {}))

Test

Added test_wrap_task_run_preserves_signature in tests/integrations/celery/test_celery.py. The test fails on master (sees args/kwargs placeholders) and passes with the fix (sees self, args, kwargs bound by name).

Why this matters downstream

At Rover.com (rover.com, ~1M+ DAU pet-services marketplace), our test suite defaults every mock.patch to autospec=True, spec_set=True (1,046 test files use this convention). With sentry-sdk 1.x we carry a FixSentryCeleryIntegration integration that wraps the private _wrap_apply_async symbol just to force wrapper.__signature__ = inspect.signature(<*args, **kwargs callable>). The same symbol exists in 2.x as _wrap_task_run, with the same gap. If this PR lands, that Rover-side monkey-patch can be deleted entirely on the next upgrade.


General Notes

Filed without a pre-existing GitHub issue per the contribution guidelines note; happy to open one and reference it if maintainers prefer. PR is opened as a draft per the contribution standards.

`_wrap_task_run` replaces `Task.apply_async` (and `Celery.send_task`)
with a wrapper declared as `def apply_async(*args, **kwargs)`. Without
an explicit `__signature__`, tools that introspect the wrapper but do
not follow `__wrapped__` (e.g. `inspect.getcallargs`) see the
wrapper's bare `(*args, **kwargs)` instead of the wrapped function's
parameter names.

Setting `wrapper.__signature__ = inspect.signature(f)` keeps the
original signature visible regardless of how callers inspect the
wrapper. This mirrors PR getsentry#3178, which applied the same fix to the
`sentry_sdk.tracing.trace` decorator.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant