Repository: sbdchd/celery-types (language: python)

Issue:
`Task.request` is not `celery.worker.request.Request` (during `task_prerun`)
`Task.request` is typed like below

https://github.com/sbdchd/celery-types/blob/af8943669754ba6b3df226a4767a886a5a8f347f/celery-stubs/app/task.pyi#L235-L236

With the following `task_prerun` receiver

```python
@signals.task_prerun.connect
def task_prerun_receiver(*args: Any, **kwargs: Any) -> None:
    task: Task = kwargs["sender"]
    ...
    logger.info("task_prerun_receiver", task_request_dict=task.request.request_dict)
```

I'm getting at traceback like

```
Traceback (most recent call last):
  File "/app/venv/lib/python3.10/site-packages/celery/utils/dispatch/signal.py", line 276, in send
    response = receiver(signal=self, sender=sender, **named)
  File "/app/src/myapp/celery/app.py", line 59, in task_prerun_receiver
    logger.info("task_prerun_receiver", task_request_dict=task.request.request_dict)
AttributeError: 'Context' object has no attribute 'request_dict'
```

Celery documents "Sender is the task object being executed"
Ref: https://docs.celeryq.dev/en/stable/userguide/signals.html#task-prerun

I thought `task.request` was a `Request` object, but at least in the case of a receiver on `task_prerun` it's rather an instance of `celery.app.task.Context`.


Discussion / hints:

Fix patch:
diff --git a/celery-stubs/app/__init__.pyi b/celery-stubs/app/__init__.pyi
index 2ee722a..9fe73f7 100644
--- a/celery-stubs/app/__init__.pyi
+++ b/celery-stubs/app/__init__.pyi
@@ -12,6 +12,7 @@ from celery.app import beat as beat
 from celery.app import control as control
 from celery.app import events as events
 from celery.app import task as task
+from celery.app.task import Context
 from celery.app.task import Task as Task
 from celery.utils.threads import _LocalStack
 from typing_extensions import ParamSpec
@@ -53,7 +54,7 @@ def shared_task(
     expires: float | datetime | None = ...,
     priority: int | None = ...,
     resultrepr_maxsize: int = ...,
-    request_stack: _LocalStack = ...,
+    request_stack: _LocalStack[Context] = ...,
     abstract: bool = ...,
     queue: str = ...,
 ) -> Callable[[Callable[_P, _R]], Task[_P, _R]]: ...
@@ -88,7 +89,7 @@ def shared_task(
     expires: float | datetime | None = ...,
     priority: int | None = ...,
     resultrepr_maxsize: int = ...,
-    request_stack: _LocalStack = ...,
+    request_stack: _LocalStack[Context] = ...,
     abstract: bool = ...,
     queue: str = ...,
 ) -> Callable[[Callable[Concatenate[Task[_P, _R], _P], _R]], Task[_P, _R]]: ...
@@ -123,7 +124,7 @@ def shared_task(
     expires: float | datetime | None = ...,
     priority: int | None = ...,
     resultrepr_maxsize: int = ...,
-    request_stack: _LocalStack = ...,
+    request_stack: _LocalStack[Context] = ...,
     abstract: bool = ...,
     queue: str = ...,
 ) -> Callable[[Callable[..., Any]], _T]: ...
diff --git a/celery-stubs/app/base.pyi b/celery-stubs/app/base.pyi
index 38a7f94..22fea9e 100644
--- a/celery-stubs/app/base.pyi
+++ b/celery-stubs/app/base.pyi
@@ -19,6 +19,7 @@ from celery.app.events import Events
 from celery.app.log import Logging
 from celery.app.registry import TaskRegistry
 from celery.app.routes import Router
+from celery.app.task import Context
 from celery.app.task import Task as CeleryTask
 from celery.app.utils import Settings
 from celery.apps.worker import Worker as CeleryWorker
@@ -132,7 +133,7 @@ class Celery:
         expires: float | datetime.datetime | None = ...,
         priority: int | None = ...,
         resultrepr_maxsize: int = ...,
-        request_stack: _LocalStack = ...,
+        request_stack: _LocalStack[Context] = ...,
         abstract: bool = ...,
         after_return: Callable[..., Any] = ...,
         on_retry: Callable[..., Any] = ...,
@@ -177,7 +178,7 @@ class Celery:
         expires: float | datetime.datetime | None = ...,
         priority: int | None = ...,
         resultrepr_maxsize: int = ...,
-        request_stack: _LocalStack = ...,
+        request_stack: _LocalStack[Context] = ...,
         abstract: bool = ...,
         queue: str = ...,
         after_return: Callable[..., Any] = ...,
@@ -215,7 +216,7 @@ class Celery:
         expires: float | datetime.datetime | None = ...,
         priority: int | None = ...,
         resultrepr_maxsize: int = ...,
-        request_stack: _LocalStack = ...,
+        request_stack: _LocalStack[Context] = ...,
         abstract: bool = ...,
         queue: str = ...,
         after_return: Callable[..., Any] = ...,
@@ -253,7 +254,7 @@ class Celery:
         expires: float | datetime.datetime | None = ...,
         priority: int | None = ...,
         resultrepr_maxsize: int = ...,
-        request_stack: _LocalStack = ...,
+        request_stack: _LocalStack[Context] = ...,
         abstract: bool = ...,
         queue: str = ...,
         after_return: Callable[..., Any] = ...,
diff --git a/celery-stubs/app/task.pyi b/celery-stubs/app/task.pyi
index 0008576..4b81435 100644
--- a/celery-stubs/app/task.pyi
+++ b/celery-stubs/app/task.pyi
@@ -1,4 +1,4 @@
-from collections.abc import Iterable, Mapping
+from collections.abc import Iterable, Mapping, Sequence
 from datetime import datetime
 from typing import (
     Any,
@@ -18,12 +18,52 @@ from celery.canvas import Signature, xmap, xstarmap
 from celery.exceptions import Retry
 from celery.result import EagerResult
 from celery.utils.threads import _LocalStack
-from celery.worker.request import Request
+from celery.worker.request import _DeliveryInfo
 from typing_extensions import ParamSpec
 
 _P = ParamSpec("_P")
 _R = TypeVar("_R")
 
+class Context:
+    args: Sequence[Any] | None
+    callbacks: Sequence[Mapping[str, Any]] | None
+    called_directly: bool
+    chain: str | None
+    chord: str | None
+    correlation_id: str | None
+    delivery_info: _DeliveryInfo | None
+    errbacks: Sequence[Mapping[str, Any]] | None
+    eta: int | None
+    expires: int | None
+    group: str | None
+    group_index: int | None
+    headers: Mapping[str, Any] | None
+    hostname: str | None
+    id: str | None
+    ignore_result: bool
+    is_eager: bool
+    kwargs: Mapping[str, Any] | None
+    logfile: str | None
+    loglevel: int | None
+    origin: Any
+    parent_id: int | None
+    properties: Any | None
+    retries: int
+    reply_to: Any
+    replaced_task_nesting: int
+    root_id: str | None
+    shadow: Any
+    taskset: str | None  # compat alias to group
+    timelimit: tuple[int, int] | tuple[None, None] | None
+    utc: bool | None
+    def __init__(self, *args: dict[str, Any], **kwargs: Any) -> None: ...
+    def update(self, *args: dict[str, Any], **kwargs: Any) -> None: ...
+    def clear(self) -> None: ...
+    def get(self, key: str, default: Any = ...) -> Any: ...
+    def as_execution_options(self) -> dict[str, Any]: ...
+    @property
+    def children(self) -> list[str]: ...
+
 class Task(Generic[_P, _R]):
     name: str
     typing: bool
@@ -46,7 +86,7 @@ class Task(Generic[_P, _R]):
     expires: float | datetime | None
     priority: int | None
     resultrepr_maxsize: int
-    request_stack: _LocalStack
+    request_stack: _LocalStack[Context]
     abstract: bool
     @classmethod
     def bind(cls, app: Celery) -> Celery: ...
@@ -100,7 +140,7 @@ class Task(Generic[_P, _R]):
     ) -> None: ...
     def signature_from_request(
         self,
-        request: Request | None = ...,
+        request: Context | None = ...,
         args: tuple[Any, ...] = ...,
         kwargs: dict[str, Any] | None = ...,
         queue: str | None = ...,
@@ -227,7 +267,7 @@ class Task(Generic[_P, _R]):
     def push_request(self, *args: _P.args, **kwargs: _P.kwargs) -> None: ...
     def pop_request(self) -> None: ...
     @property
-    def request(self) -> Request: ...
+    def request(self) -> Context: ...
     @property
     def backend(self) -> Backend: ...
     @backend.setter
diff --git a/celery-stubs/utils/threads.pyi b/celery-stubs/utils/threads.pyi
index bd4deab..d073567 100644
--- a/celery-stubs/utils/threads.pyi
+++ b/celery-stubs/utils/threads.pyi
@@ -1,1 +1,6 @@
-class _LocalStack: ...
+from typing import Generic, TypeVar
+
+_T = TypeVar("_T")
+
+class _LocalStack(Generic[_T]):
+    top: _T | None


Test patch:
diff --git a/tests/test_celery.py b/tests/test_celery.py
index 7b6fdcd..e7d0616 100644
--- a/tests/test_celery.py
+++ b/tests/test_celery.py
@@ -75,7 +75,8 @@ def send_twitter_status(self: Task[Any, Any], oauth: str, tweet: str) -> None:
         if exc.errno == errno.ENOMEM:
             raise Reject(exc, requeue=False)
 
-        if not self.request.delivery_info["redelivered"]:
+        delivery_info = self.request.delivery_info or {}
+        if not delivery_info.get("redelivered"):
             raise Reject("no reason", requeue=True)
 
     self.update_state(state="SUCCESS", meta={"foo": "bar"})