Skip to content

Commit 66d3e6d

Browse files
committed
Raise exception when accessing exception details on unfinished task
1 parent fae9cde commit 66d3e6d

File tree

7 files changed

+66
-20
lines changed

7 files changed

+66
-20
lines changed

django_tasks/backends/database/models.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ def task_result(self) -> "TaskResult[T]":
152152
except ImportError:
153153
exception_class = None
154154

155-
return TaskResult[T](
155+
task_result = TaskResult[T](
156156
db_result=self,
157157
task=self.task,
158158
id=normalize_uuid(self.id),
@@ -163,10 +163,13 @@ def task_result(self) -> "TaskResult[T]":
163163
args=self.args_kwargs["args"],
164164
kwargs=self.args_kwargs["kwargs"],
165165
backend=self.backend_name,
166-
exception_class=exception_class,
167-
traceback=self.traceback or None,
168166
)
169167

168+
object.__setattr__(task_result, "_exception_class", exception_class)
169+
object.__setattr__(task_result, "_traceback", self.traceback or None)
170+
171+
return task_result
172+
170173
@retry(backoff_delay=0)
171174
def claim(self) -> None:
172175
"""

django_tasks/backends/dummy.py

-2
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,6 @@ def enqueue(
4747
args=args,
4848
kwargs=kwargs,
4949
backend=self.alias,
50-
exception_class=None,
51-
traceback=None,
5250
)
5351

5452
if self._get_enqueue_on_commit_for_task(task) is not False:

django_tasks/backends/immediate.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ def _execute_task(self, task_result: TaskResult) -> None:
5353

5454
object.__setattr__(task_result, "finished_at", timezone.now())
5555

56-
object.__setattr__(task_result, "traceback", get_exception_traceback(e))
57-
object.__setattr__(task_result, "exception_class", type(e))
56+
object.__setattr__(task_result, "_traceback", get_exception_traceback(e))
57+
object.__setattr__(task_result, "_exception_class", type(e))
5858

5959
object.__setattr__(task_result, "status", ResultStatus.FAILED)
6060

@@ -80,8 +80,6 @@ def enqueue(
8080
args=args,
8181
kwargs=kwargs,
8282
backend=self.alias,
83-
exception_class=None,
84-
traceback=None,
8583
)
8684

8785
if self._get_enqueue_on_commit_for_task(task) is not False:

django_tasks/task.py

+26-11
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@
3737
DEFAULT_PRIORITY = 0
3838

3939
TASK_REFRESH_ATTRS = {
40-
"exception_class",
41-
"traceback",
40+
"_exception_class",
41+
"_traceback",
4242
"_return_value",
4343
"finished_at",
4444
"started_at",
@@ -255,11 +255,8 @@ class TaskResult(Generic[T]):
255255
backend: str
256256
"""The name of the backend the task will run on"""
257257

258-
exception_class: Optional[Type[BaseException]]
259-
"""The exception raised by the task function"""
260-
261-
traceback: Optional[str]
262-
"""The traceback of the exception if the task failed"""
258+
_exception_class: Optional[Type[BaseException]] = field(init=False, default=None)
259+
_traceback: Optional[str] = field(init=False, default=None)
263260

264261
_return_value: Optional[T] = field(init=False, default=None)
265262

@@ -271,14 +268,32 @@ def return_value(self) -> Optional[T]:
271268
If the task didn't succeed, an exception is raised.
272269
This is to distinguish against the task returning None.
273270
"""
274-
if self.status == ResultStatus.FAILED:
275-
raise ValueError("Task failed")
276-
277-
elif self.status != ResultStatus.SUCCEEDED:
271+
if not self.is_finished:
278272
raise ValueError("Task has not finished yet")
279273

280274
return cast(T, self._return_value)
281275

276+
@property
277+
def exception_class(self) -> Optional[Type[BaseException]]:
278+
"""The exception raised by the task function"""
279+
if not self.is_finished:
280+
raise ValueError("Task has not finished yet")
281+
282+
return self._exception_class
283+
284+
@property
285+
def traceback(self) -> Optional[str]:
286+
"""The traceback of the exception if the task failed"""
287+
if not self.is_finished:
288+
raise ValueError("Task has not finished yet")
289+
290+
return self._traceback
291+
292+
@property
293+
def is_finished(self) -> bool:
294+
"""Has the task finished?"""
295+
return self.status in {ResultStatus.FAILED, ResultStatus.SUCCEEDED}
296+
282297
def refresh(self) -> None:
283298
"""
284299
Reload the cached task data from the task store

tests/tests/test_database_backend.py

+18
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ def test_enqueue_task(self) -> None:
7878
result = cast(Task, task).enqueue(1, two=3)
7979

8080
self.assertEqual(result.status, ResultStatus.NEW)
81+
self.assertFalse(result.is_finished)
8182
self.assertIsNone(result.started_at)
8283
self.assertIsNone(result.finished_at)
8384
with self.assertRaisesMessage(ValueError, "Task has not finished yet"):
@@ -92,6 +93,7 @@ async def test_enqueue_task_async(self) -> None:
9293
result = await cast(Task, task).aenqueue()
9394

9495
self.assertEqual(result.status, ResultStatus.NEW)
96+
self.assertFalse(result.is_finished)
9597
self.assertIsNone(result.started_at)
9698
self.assertIsNone(result.finished_at)
9799
with self.assertRaisesMessage(ValueError, "Task has not finished yet"):
@@ -128,13 +130,15 @@ def test_refresh_result(self) -> None:
128130
)
129131

130132
self.assertEqual(result.status, ResultStatus.NEW)
133+
self.assertFalse(result.is_finished)
131134
self.assertIsNone(result.started_at)
132135
self.assertIsNone(result.finished_at)
133136
with self.assertNumQueries(1):
134137
result.refresh()
135138
self.assertIsNotNone(result.started_at)
136139
self.assertIsNotNone(result.finished_at)
137140
self.assertEqual(result.status, ResultStatus.SUCCEEDED)
141+
self.assertTrue(result.is_finished)
138142

139143
async def test_refresh_result_async(self) -> None:
140144
result = await default_task_backend.aenqueue(
@@ -148,12 +152,14 @@ async def test_refresh_result_async(self) -> None:
148152
)
149153

150154
self.assertEqual(result.status, ResultStatus.NEW)
155+
self.assertFalse(result.is_finished)
151156
self.assertIsNone(result.started_at)
152157
self.assertIsNone(result.finished_at)
153158
await result.arefresh()
154159
self.assertIsNotNone(result.started_at)
155160
self.assertIsNotNone(result.finished_at)
156161
self.assertEqual(result.status, ResultStatus.SUCCEEDED)
162+
self.assertTrue(result.is_finished)
157163

158164
def test_get_missing_result(self) -> None:
159165
with self.assertRaises(ResultDoesNotExist):
@@ -464,6 +470,12 @@ def test_failing_task(self) -> None:
464470
result = test_tasks.failing_task_value_error.enqueue()
465471
self.assertEqual(DBTaskResult.objects.ready().count(), 1)
466472

473+
with self.assertRaisesMessage(ValueError, "Task has not finished yet"):
474+
result.exception_class # noqa: B018
475+
476+
with self.assertRaisesMessage(ValueError, "Task has not finished yet"):
477+
result.traceback # noqa: B018
478+
467479
with self.assertNumQueries(9 if connection.vendor == "mysql" else 8):
468480
self.run_worker()
469481

@@ -490,6 +502,12 @@ def test_complex_exception(self) -> None:
490502
result = test_tasks.complex_exception.enqueue()
491503
self.assertEqual(DBTaskResult.objects.ready().count(), 1)
492504

505+
with self.assertRaisesMessage(ValueError, "Task has not finished"):
506+
result.exception_class # noqa: B018
507+
508+
with self.assertRaisesMessage(ValueError, "Task has not finished"):
509+
result.traceback # noqa: B018
510+
493511
with self.assertNumQueries(9 if connection.vendor == "mysql" else 8):
494512
self.run_worker()
495513

tests/tests/test_dummy_backend.py

+11
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ def test_enqueue_task(self) -> None:
3333
result = cast(Task, task).enqueue(1, two=3)
3434

3535
self.assertEqual(result.status, ResultStatus.NEW)
36+
self.assertFalse(result.is_finished)
3637
self.assertIsNone(result.started_at)
3738
self.assertIsNone(result.finished_at)
3839
with self.assertRaisesMessage(ValueError, "Task has not finished yet"):
@@ -49,6 +50,7 @@ async def test_enqueue_task_async(self) -> None:
4950
result = await cast(Task, task).aenqueue()
5051

5152
self.assertEqual(result.status, ResultStatus.NEW)
53+
self.assertFalse(result.is_finished)
5254
self.assertIsNone(result.started_at)
5355
self.assertIsNone(result.finished_at)
5456
with self.assertRaisesMessage(ValueError, "Task has not finished yet"):
@@ -151,6 +153,15 @@ def test_enqueue_logs(self) -> None:
151153
self.assertIn("enqueued", captured_logs.output[0])
152154
self.assertIn(result.id, captured_logs.output[0])
153155

156+
def test_exceptions(self) -> None:
157+
result = test_tasks.noop_task.enqueue()
158+
159+
with self.assertRaisesMessage(ValueError, "Task has not finished yet"):
160+
result.exception_class # noqa: B018
161+
162+
with self.assertRaisesMessage(ValueError, "Task has not finished yet"):
163+
result.traceback # noqa: B018
164+
154165

155166
class DummyBackendTransactionTestCase(TransactionTestCase):
156167
@override_settings(

tests/tests/test_immediate_backend.py

+3
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ def test_enqueue_task(self) -> None:
3131
result = cast(Task, task).enqueue(1, two=3)
3232

3333
self.assertEqual(result.status, ResultStatus.SUCCEEDED)
34+
self.assertTrue(result.is_finished)
3435
self.assertIsNotNone(result.started_at)
3536
self.assertIsNotNone(result.finished_at)
3637
self.assertGreaterEqual(result.started_at, result.enqueued_at) # type:ignore[arg-type, misc]
@@ -46,6 +47,7 @@ async def test_enqueue_task_async(self) -> None:
4647
result = await cast(Task, task).aenqueue()
4748

4849
self.assertEqual(result.status, ResultStatus.SUCCEEDED)
50+
self.assertTrue(result.is_finished)
4951
self.assertIsNotNone(result.started_at)
5052
self.assertIsNotNone(result.finished_at)
5153
self.assertGreaterEqual(result.started_at, result.enqueued_at) # type:ignore[arg-type, misc]
@@ -80,6 +82,7 @@ def test_catches_exception(self) -> None:
8082

8183
# assert result
8284
self.assertEqual(result.status, ResultStatus.FAILED)
85+
self.assertTrue(result.is_finished)
8386
self.assertIsNotNone(result.started_at)
8487
self.assertIsNotNone(result.finished_at)
8588
self.assertGreaterEqual(result.started_at, result.enqueued_at) # type:ignore[arg-type, misc]

0 commit comments

Comments
 (0)