Opened 4 years ago

Closed 3 years ago

Last modified 2 years ago

#25761 closed Bug (fixed)

Re-raised exceptions with __cause__ should also set __traceback__ on the exception

Reported by: Raphaël Hertzog Owned by: nobody
Component: Database layer (models, ORM) Version: 1.8
Severity: Normal Keywords:
Cc: Triage Stage: Accepted
Has patch: yes Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

In bug https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=802677 we have a report that testtools is broken when handling database exceptions raised by Django. This has been brought to the upstream developers of testtools who were quick to point out that Django is at fault:
https://github.com/testing-cabal/testtools/issues/162#issuecomment-156891988

Django is setting the __cause__ attribute thus mimicking Python's 3 behaviour, but not ensuring that the associated exception has a usable __traceback__ attribute. The traceback2 module (backport of Python 3's traceback) relies on this... thus Django should also make sure that the exception has the expected attribute when it sets __cause__.

This needs to be fixed at least in django/db/utils.py (DatabaseErrorWrapper.__exit__()). But there are other cases where Django is setting __cause___ in django/db/migrations/loader.py,
django/utils/dictconfig.py and django/utils/timezone.py. It looks like they might have the same problem.

A fix for the first case might possibly be something like this (untested):

--- a/django/db/utils.py
+++ b/django/db/utils.py
@@ -91,6 +91,8 @@ class DatabaseErrorWrapper(object):
             if issubclass(exc_type, db_exc_type):
                 dj_exc_value = dj_exc_type(*exc_value.args)
                 dj_exc_value.__cause__ = exc_value
+                if not hasattr(exc_value, '__traceback__'):
+                    setattr(exc_value, '__traceback__', traceback)
                 # Only set the 'errors_occurred' flag for errors that may make
                 # the connection unusable.
                 if dj_exc_type not in (DataError, IntegrityError):

Change History (6)

comment:1 Changed 4 years ago by Tim Graham

Severity: Release blockerNormal
Triage Stage: UnreviewedAccepted

There is also some documentation to be updated. See the patch from #17601.

comment:2 Changed 3 years ago by Raphaël Hertzog

Here's a pull request fixing this ticket: https://github.com/django/django/pull/5721

The test suite passes but I have not tested yet if it really fixes the issue originally reported to Debian. Also master no longer has django/utils/dictconfig.py and you might want to fix that occurrence as well when you backport the fix to 1.8.x / 1.9.x.

comment:3 Changed 3 years ago by Claude Paroz

Has patch: set

comment:4 Changed 3 years ago by Tim Graham

The bug fix doesn't seem to qualify for a backport since the issue has been around as long as Django has supported Python 3 and testtools should be able to prevent the crash.

comment:5 Changed 3 years ago by Tim Graham <timograham@…>

Resolution: fixed
Status: newclosed

In 9f4e031b:

Fixed #25761 -- Added cause.traceback to reraised exceptions.

When Django reraises an exception, it sets the cause attribute even
in Python 2, mimicking Python's 3 behavior for "raise Foo from Bar".
However, Python 3 also ensures that all exceptions have a traceback
attribute and thus the "traceback2" Python 2 module (backport of Python
3's "traceback" module) relies on the fact that whenever you have a
cause attribute, the recorded exception also has a traceback
attribute.

This is breaking testtools which is using traceback2 (see
https://github.com/testing-cabal/testtools/issues/162).

This commit fixes this inconsistency by ensuring that Django sets
the traceback attribute on any exception stored in a cause
attribute of a reraised exception.

comment:6 Changed 2 years ago by GitHub <noreply@…>

In 435e4bf3:

Refs #23919 -- Removed traceback setting needed for Python 2.

Partially reverted refs #25761 and refs #16245.

Note: See TracTickets for help on using tickets.
Back to Top