Repository: django-extensions/django-extensions
Pull request #1837 (2023-09-15 16:53:13)

Call post_command when the command raises an unhandled exception
`post_command` is currently not called if an exception is raised which can cause problems if we really need it.


Patch:
diff --git a/django_extensions/management/utils.py b/django_extensions/management/utils.py
index 3a5ed51d..9bdeb327 100644
--- a/django_extensions/management/utils.py
+++ b/django_extensions/management/utils.py
@@ -59,8 +59,13 @@ def signalcommand(func):
 
     def inner(self, *args, **kwargs):
         pre_command.send(self.__class__, args=args, kwargs=kwargs)
-        ret = func(self, *args, **kwargs)
-        post_command.send(self.__class__, args=args, kwargs=kwargs, outcome=ret)
+        try:
+            ret = func(self, *args, **kwargs)
+        except Exception as e:
+            post_command.send(self.__class__, args=args, kwargs=kwargs, outcome=e)
+            raise
+        else:
+            post_command.send(self.__class__, args=args, kwargs=kwargs, outcome=ret)
         return ret
     return inner
 
diff --git a/django_extensions/settings.py b/django_extensions/settings.py
index 4a2401b0..eb1bc473 100644
--- a/django_extensions/settings.py
+++ b/django_extensions/settings.py
@@ -9,10 +9,12 @@ REPLACEMENTS = getattr(settings, 'EXTENSIONS_REPLACEMENTS', {})
 DEFAULT_SQLITE_ENGINES = (
     'django.db.backends.sqlite3',
     'django.db.backends.spatialite',
+    'django_prometheus.db.backends.sqlite3',
 )
 DEFAULT_MYSQL_ENGINES = (
     'django.db.backends.mysql',
     'django.contrib.gis.db.backends.mysql',
+    'django_prometheus.db.backends.mysql',
     'mysql.connector.django',
 )
 DEFAULT_POSTGRESQL_ENGINES = (
@@ -23,6 +25,8 @@ DEFAULT_POSTGRESQL_ENGINES = (
     'psqlextra.backend',
     'django_zero_downtime_migrations.backends.postgres',
     'django_zero_downtime_migrations.backends.postgis',
+    'django_prometheus.db.backends.postgresql',
+    'django_prometheus.db.backends.postgis',
     'django_tenants.postgresql_backend',
 )
 


Test patch:
diff --git a/tests/test_management_command.py b/tests/test_management_command.py
index 1fc7bdf9..799b65f6 100644
--- a/tests/test_management_command.py
+++ b/tests/test_management_command.py
@@ -4,12 +4,13 @@ from unittest import mock
 import logging
 import importlib
 
-from django.core.management import call_command, find_commands, load_command_class
+from django.core.management import call_command, find_commands, load_command_class, BaseCommand
 from django.test import TestCase
 from io import StringIO
 
 from django_extensions.management.modelviz import use_model, generate_graph_data
 from django_extensions.management.commands.merge_model_instances import get_model_to_deduplicate, get_field_names, keep_first_or_last_instance
+from django_extensions.management.utils import signalcommand
 from . import force_color_support
 from .testapp.models import Person, Name, Note, Personality, Club, Membership, Permission
 from .testapp.jobs.hourly.test_hourly_job import HOURLY_JOB_MOCK
@@ -104,6 +105,38 @@ class CommandSignalTests(TestCase):
         self.assertIn('kwargs', CommandSignalTests.post)
         self.assertIn('outcome', CommandSignalTests.post)
 
+    def test_exception(self):
+        from django_extensions.management.signals import post_command, pre_command
+
+        exception = Exception()
+
+        class TestCommand(BaseCommand):
+            @signalcommand
+            def handle(self, *args, **options):
+                raise exception
+
+        def pre(sender, **kwargs):
+            CommandSignalTests.pre = dict(**kwargs)
+
+        def post(sender, **kwargs):
+            CommandSignalTests.post = dict(**kwargs)
+
+        pre_command.connect(pre, TestCommand)
+        post_command.connect(post, TestCommand)
+
+        out = StringIO()
+
+        with self.assertRaises(Exception) as e_info:
+            call_command(TestCommand(), stdout=out)
+        self.assertEqual(exception, e_info.exception)
+        self.assertIn('args', CommandSignalTests.pre)
+        self.assertIn('kwargs', CommandSignalTests.pre)
+
+        self.assertIn('args', CommandSignalTests.post)
+        self.assertIn('kwargs', CommandSignalTests.post)
+        self.assertIn('outcome', CommandSignalTests.post)
+        self.assertEqual(e_info.exception, CommandSignalTests.post["outcome"])
+
 
 class CommandClassTests(TestCase):
     def setUp(self):