Skip to content

Instantly share code, notes, and snippets.

@danielsousaio
Last active September 27, 2019 19:57
Show Gist options
  • Save danielsousaio/f54efc1774af491e44292d102856d534 to your computer and use it in GitHub Desktop.
Save danielsousaio/f54efc1774af491e44292d102856d534 to your computer and use it in GitHub Desktop.
sys.exit(3) on background job that runs makemigrations - infinite loop

This gist proposes a solution to this issue by doing the same checks Django uses to either ask further questions during migrations creation or not. Currently our background job stays in a loop because we don't provide a valid answer to this questioner. If we use the flag --no-input it isn't enough as 2 scenarios still raise the questioner no matter what.

Calling makemigrations can call either InteractiveMigrationQuestioner or NonInteractiveMigrationQuestioner. When we send the flag --no-input to the makemigrations command it calls the non-interactive questioner.

Source code:

if self.interactive:
    questioner = InteractiveMigrationQuestioner(specified_apps=app_labels, dry_run=self.dry_run)
else:
    questioner = NonInteractiveMigrationQuestioner(specified_apps=app_labels, dry_run=self.dry_run)

NonInteractiveMigrationQuestioner itself will return sys.exit(3) in two methods.

Source code:

class NonInteractiveMigrationQuestioner(MigrationQuestioner):

    def ask_not_null_addition(self, field_name, model_name):
        # We can't ask the user, so act like the user aborted.
        sys.exit(3)

    def ask_not_null_alteration(self, field_name, model_name):
        # We can't ask the user, so set as not provided.
        return NOT_PROVIDED

    def ask_auto_now_add_addition(self, field_name, model_name):
        # We can't ask the user, so act like the user aborted.
        sys.exit(3)

With that in mind, if we want to prevent a exit code other than 0 (successful exit) we have to avoid the call of:

  • ask_auto_now_add_addition
  • ask_not_null_addition

ask_not_null_alteration doesn't fail but expects us to have a sql script to run after to set all records field value to NULL.

Source code:

time_fields = (models.DateField, models.DateTimeField, models.TimeField)
preserve_default = (
	field.null or field.has_default() or field.many_to_many or
	(field.blank and field.empty_strings_allowed) or
	(isinstance(field, time_fields) and field.auto_now)
)
if not preserve_default:
	field = field.clone()
	if isinstance(field, time_fields) and field.auto_now_add:
		field.default = self.questioner.ask_auto_now_add_addition(field_name, model_name)
	else:
		field.default = self.questioner.ask_not_null_addition(field_name, model_name)

TL;DR: Above is all the logic Django uses to check if the questioner should display or not. Checks such as is the field nullable? Does the field have a default? We could do the same checks and rest assured that the non-interactive makemigrations never fails. I also suggested earlier that if the request to the model builder fails this very same checks that the API should reply with an error stating that something more is required (whatever is missing).

UPDATE:

This check should also be made:

if (old_field.null and not new_field.null and not new_field.has_default() and not new_field.many_to_many):

To avoid the call of ask_not_null_alteration. The source code explains why this is a good idea:

def ask_not_null_alteration(self, field_name, model_name):
	"""Changing a NULL field to NOT NULL."""
	if not self.dry_run:
		choice = self._choice_input(
			"You are trying to change the nullable field '%s' on %s to non-nullable "
			"without a default; we can't do that (the database needs something to "
			"populate existing rows).\n"
			"Please select a fix:" % (field_name, model_name),
			[
				("Provide a one-off default now (will be set on all existing "
				  "rows with a null value for this column)"),
				("Ignore for now, and let me handle existing rows with NULL myself "
				  "(e.g. because you added a RunPython or RunSQL operation to handle "
				  "NULL values in a previous data migration)"),
				"Quit, and let me add a default in models.py",
			]
		)
		if choice == 2:
			return NOT_PROVIDED
		elif choice == 3:
			sys.exit(3)
		else:
			return self._ask_default()
	return None
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment