How can I disable exception chaining in Python?

Viewed 69

I have a block of code that looks like this:

class MigrationsCreatedTest(TestCase):
    def test_migrations_created(self):
        try:
            call_command("makemigrations", "--check", "--dry-run")
        except SystemExit:
            raise Exception("There are migrations that have not been created!")

This is a Django test to make sure I didn't forget to run ./manage.py makemigrations.

call_command will call sys.exit(1) if there are migration files that have not been created (if the test fails), which the unit test framework won't catch like it does for almost all other types of exceptions, so I catch it explicitly here.

This works, but the slightly annoying part is that the output looks like this:

======================================================================
ERROR: test_migrations_created (tests.test_migrations_created.MigrationsCreatedTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/user/ahs-project/website/tests/test_migrations_created.py", line 10, in test_migrations_created
    call_command(
  File "/home/user/ahs-project/venv/lib/python3.9/site-packages/django/core/management/__init__.py", line 198, in call_command
    return command.execute(*args, **defaults)
  File "/home/user/ahs-project/venv/lib/python3.9/site-packages/django/core/management/base.py", line 460, in execute
    output = self.handle(*args, **options)
  File "/home/user/ahs-project/venv/lib/python3.9/site-packages/django/core/management/base.py", line 98, in wrapped
    res = handle_func(*args, **kwargs)
  File "/home/user/ahs-project/venv/lib/python3.9/site-packages/django/core/management/commands/makemigrations.py", line 216, in handle
    sys.exit(1)
SystemExit: 1

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/user/ahs-project/website/tests/test_migrations_created.py", line 16, in test_migrations_created
    raise Exception("There are migrations that have not been created!")
Exception: There are migrations that have not been created!

Python remembers that the original exception was a SystemExit exception, which makes the output really noisy. It's unnecessary info here.

How can I tell Python to forget the original exception raised, and just output info about the new exception being raised?

Also, why is Python even doing this? I thought the only time it would remember the previous exception raised was if you explicitly opt into it with raise ... from exc?

2 Answers

I don't think the issue here is that it's too noisy, but that the error message is kind of misleading. "During handling of the above exception, another exception occurred" implies that an exception happened while handling the SystemExit exception, which is not the case; you were merely raising an exception with a more informative message.

The idiomatic way to do this would be to change your raise to a raise from, like this:

class MigrationsCreatedTest(TestCase):
    def test_migrations_created(self):
        try:
            call_command("makemigrations", "--check", "--dry-run")
        except SystemExit as e:
            raise Exception("There are migrations that have not been created!") from e

And this will cause the exception stack trace. (Ignore the ipykernel parts - that's just because I ran this from a Jupyter notebook).

---------------------------------------------------------------------------
SystemExit                                Traceback (most recent call last)
/var/folders/z0/1jq2d9yj5_vd_nzbzvy24jjr0000gn/T/ipykernel_36864/4044027728.py in test_migrations_created(self)
     10         try:
---> 11             call_command("makemigrations", "--check", "--dry-run")
     12         except SystemExit as e:

/var/folders/z0/1jq2d9yj5_vd_nzbzvy24jjr0000gn/T/ipykernel_36864/4044027728.py in call_command(*a)
      5 def call_command(*a):
----> 6     raise SystemExit()
      7

SystemExit:

The above exception was the direct cause of the following exception:

Exception                                 Traceback (most recent call last)
/var/folders/z0/1jq2d9yj5_vd_nzbzvy24jjr0000gn/T/ipykernel_36864/4044027728.py in <module>
     14
     15
---> 16 MigrationsCreatedTest().test_migrations_created()

/var/folders/z0/1jq2d9yj5_vd_nzbzvy24jjr0000gn/T/ipykernel_36864/4044027728.py in test_migrations_created(self)
     11             call_command("makemigrations", "--check", "--dry-run")
     12         except SystemExit as e:
---> 13             raise Exception("There are migrations that have not been created!") from e
     14
     15

Exception: There are migrations that have not been created!

"The above exception was the direct cause of the following exception" conveys what's going on better than "During handling of the above exception, another exception occurred".

It's a bit hacky but this seems to work:

def raises_an_exception():
    raise SystemExit("Original Exception")

def catches_and_raises_a_new_exception():
    original_exception = None
    try:
        raises_an_exception()
    except SystemExit as system_exit:
        original_exception = system_exit

    if original_exception != None:
        raise Exception("New Exception")

catches_and_raises_a_new_exception()
Related