Repository: django/django
Issue category: Feature Request

Issue:
Loop on all errors in `InteractiveMigrationQuestioner._ask_default()`
Description
	 
When changing some fields, the migration questioner can ask for new field default values entered through a Python prompt.
Yesterday, I made a typo which made the makemigrations process exit. This was a little frustrating as there were several fields to fix, and my work answering for the first fields was lost.
Currently, the questioner loops on SyntaxError and NameError (​source). My mistake lead to an AttributeError, hence the crash and lost work. I propose we extend this clause to Exception - there's little harm in displaying the error and looping.
Here's a reproducing example. Say you drop null=True from two model fields:
example/models.py
diff --git example/models.py example/models.py
index 816fe37..9743877 100644
			  
			
22
33
44class Book(models.Model):
5     title = models.CharField(max_length=100, null=True)
6     published_date = models.DateField(null=True)
 5    title = models.CharField(max_length=100)
 6    published_date = models.DateField()
makemigrations asks you about the first one:
$ ./manage.py makemigrations example
It is impossible to change a nullable field 'title' on book to non-nullable without providing a default. This is because the database needs something to populate existing rows.
Please select a fix:
 1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
 2) Ignore for now. Existing rows that contain NULL values will have to be handled manually, for example with a RunPython or RunSQL operation.
 3) Quit and manually define a default value in models.py.
Select an option: 1
Please enter the default value as valid Python.
The datetime and django.utils.timezone modules are available, so it is possible to provide e.g. timezone.now as a value.
Type 'exit' to exit this prompt
>>> "Unknown book"
Immediately after this, it asks about the second field. Making a typo like .dat() instead of .date() crashes the process:
It is impossible to change a nullable field 'published_date' on book to non-nullable without providing a default. This is because the database needs something to populate existing rows.
Please select a fix:
 1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
 2) Ignore for now. Existing rows that contain NULL values will have to be handled manually, for example with a RunPython or RunSQL operation.
 3) Quit and manually define a default value in models.py.
Select an option: 1
Please enter the default value as valid Python.
The datetime and django.utils.timezone modules are available, so it is possible to provide e.g. timezone.now as a value.
Type 'exit' to exit this prompt
>>> datetime.dat(1970, 1, 1)
Traceback (most recent call last):
 File "/..././manage.py", line 21, in <module>
	main()
 ...
 File "/.../.venv/.../django/db/migrations/questioner.py", line 162, in _ask_default
	return eval(code, {}, {"datetime": datetime, "timezone": timezone})
		 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 File "<string>", line 1, in <module>
AttributeError: module 'datetime' has no attribute 'dat'. Did you mean: 'date'?
But if you instead made a typo in the datetime name, it would loop:
>>> datetme.date(1970,1,1)
Invalid input: name 'datetme' is not defined
>>>


Hints:
['As long as KeyboardInterrupt(BaseException) is allowed to bubble up (which catching for Exception allows) I agree that not trying to catch a predefined set of exceptions is a better default.', 1730539235.0]
['In 3434fab7: Refs #35882 -- Added test for migration questioner KeyboardInterrupt.', 1731917749.0]
['In e035db1: Fixed #35882 -- Made migration questioner loop on all errors.', 1731917750.0]

Patch:
diff --git a/django/db/migrations/questioner.py b/django/db/migrations/questioner.py
index e1081ab70ac2..2e6119558188 100644
--- a/django/db/migrations/questioner.py
+++ b/django/db/migrations/questioner.py
@@ -160,8 +160,8 @@ def _ask_default(self, default=""):
             else:
                 try:
                     return eval(code, {}, {"datetime": datetime, "timezone": timezone})
-                except (SyntaxError, NameError) as e:
-                    self.prompt_output.write("Invalid input: %s" % e)
+                except Exception as e:
+                    self.prompt_output.write(f"{e.__class__.__name__}: {e}")
 
     def ask_not_null_addition(self, field_name, model_name):
         """Adding a NOT NULL field to a model."""


Test patch:
diff --git a/tests/migrations/test_questioner.py b/tests/migrations/test_questioner.py
index c1aebcb22491..ec1013923b06 100644
--- a/tests/migrations/test_questioner.py
+++ b/tests/migrations/test_questioner.py
@@ -61,10 +61,32 @@ def test_questioner_no_default_no_user_entry(self, mock_input):
         )
 
     @mock.patch("builtins.input", side_effect=["bad code", "exit"])
-    def test_questioner_no_default_bad_user_entry_code(self, mock_input):
+    def test_questioner_no_default_syntax_error(self, mock_input):
         with self.assertRaises(SystemExit):
             self.questioner._ask_default()
-        self.assertIn("Invalid input: ", self.prompt.getvalue())
+        self.assertIn("SyntaxError: invalid syntax", self.prompt.getvalue())
+
+    @mock.patch("builtins.input", side_effect=["datetim", "exit"])
+    def test_questioner_no_default_name_error(self, mock_input):
+        with self.assertRaises(SystemExit):
+            self.questioner._ask_default()
+        self.assertIn(
+            "NameError: name 'datetim' is not defined", self.prompt.getvalue()
+        )
+
+    @mock.patch("builtins.input", side_effect=["datetime.dat", "exit"])
+    def test_questioner_no_default_attribute_error(self, mock_input):
+        with self.assertRaises(SystemExit):
+            self.questioner._ask_default()
+        self.assertIn(
+            "AttributeError: module 'datetime' has no attribute 'dat'",
+            self.prompt.getvalue(),
+        )
+
+    @mock.patch("builtins.input", side_effect=[KeyboardInterrupt()])
+    def test_questioner_no_default_keyboard_interrupt(self, mock_input):
+        with self.assertRaises(KeyboardInterrupt):
+            self.questioner._ask_default()
 
     @mock.patch("builtins.input", side_effect=["", "n"])
     def test_questioner_no_default_no_user_entry_boolean(self, mock_input):