Coverage for /Users/davegaeddert/Development/dropseed/plain/plain-models/plain/models/migrations/autodetector.py: 9%

1 import functools

2 import re

3 from graphlib import TopologicalSorter

4 from itertools import chain

5

6 from plain import models

7 from plain.models.migrations import operations

8 from plain.models.migrations.migration import Migration

9 from plain.models.migrations.operations.models import AlterModelOptions

10 from plain.models.migrations.optimizer import MigrationOptimizer

11 from plain.models.migrations.questioner import MigrationQuestioner

12 from plain.models.migrations.utils import (

13 COMPILED_REGEX_TYPE,

14 RegexObject,

15 resolve_relation,

16)

17 from plain.runtime import settings

18

19

20 class MigrationAutodetector:

21 """

22 Take a pair of ProjectStates and compare them to see what the first would

23 need doing to make it match the second (the second usually being the

24 project's current state).

25

26 Note that this naturally operates on entire projects at a time,

27 as it's likely that changes interact (for example, you can't

28 add a ForeignKey without having a migration to add the table it

29 depends on first). A user interface may offer single-app usage

30 if it wishes, with the caveat that it may not always be possible.

31 """

32

33 def __init__(self, from_state, to_state, questioner=None):

34 self.from_state = from_state

35 self.to_state = to_state

36 self.questioner = questioner or MigrationQuestioner()

37 self.existing_packages = {app for app, model in from_state.models}

38

39 def changes(

40 self, graph, trim_to_packages=None, convert_packages=None, migration_name=None

41 ):

42 """

43 Main entry point to produce a list of applicable changes.

44 Take a graph to base names on and an optional set of packages

45 to try and restrict to (restriction is not guaranteed)

46 """

47 changes = self._detect_changes(convert_packages, graph)

48 changes = self.arrange_for_graph(changes, graph, migration_name)

49 if trim_to_packages:

50 changes = self._trim_to_packages(changes, trim_to_packages)

51 return changes

52

53 def deep_deconstruct(self, obj):

54 """

55 Recursive deconstruction for a field and its arguments.

56 Used for full comparison for rename/alter; sometimes a single-level

57 deconstruction will not compare correctly.

58 """

59 if isinstance(obj, list):

60 return [self.deep_deconstruct(value) for value in obj]

61 elif isinstance(obj, tuple):

62 return tuple(self.deep_deconstruct(value) for value in obj)

63 elif isinstance(obj, dict):

64 return {key: self.deep_deconstruct(value) for key, value in obj.items()}

65 elif isinstance(obj, functools.partial):

66 return (

67 obj.func,

68 self.deep_deconstruct(obj.args),

69 self.deep_deconstruct(obj.keywords),

70 )

71 elif isinstance(obj, COMPILED_REGEX_TYPE):

72 return RegexObject(obj)

73 elif isinstance(obj, type):

74 # If this is a type that implements 'deconstruct' as an instance method,

75 # avoid treating this as being deconstructible itself - see #22951

76 return obj

77 elif hasattr(obj, "deconstruct"):

78 deconstructed = obj.deconstruct()

79 if isinstance(obj, models.Field):

80 # we have a field which also returns a name

81 deconstructed = deconstructed[1:]

82 path, args, kwargs = deconstructed

83 return (

84 path,

85 [self.deep_deconstruct(value) for value in args],

86 {key: self.deep_deconstruct(value) for key, value in kwargs.items()},

87 )

88 else:

89 return obj

90

91 def only_relation_agnostic_fields(self, fields):

92 """

93 Return a definition of the fields that ignores field names and

94 what related fields actually relate to. Used for detecting renames (as

95 the related fields change during renames).

96 """

97 fields_def = []

98 for name, field in sorted(fields.items()):

99 deconstruction = self.deep_deconstruct(field)

100 if field.remote_field and field.remote_field.model:

101 deconstruction[2].pop("to", None)

102 fields_def.append(deconstruction)

103 return fields_def

104

105 def _detect_changes(self, convert_packages=None, graph=None):

106 """

107 Return a dict of migration plans which will achieve the

108 change from from_state to to_state. The dict has app labels

109 as keys and a list of migrations as values.

110

111 The resulting migrations aren't specially named, but the names

112 do matter for dependencies inside the set.

113

114 convert_packages is the list of packages to convert to use migrations

115 (i.e. to make initial migrations for, in the usual case)

116

117 graph is an optional argument that, if provided, can help improve

118 dependency generation and avoid potential circular dependencies.

119 """

120 # The first phase is generating all the operations for each app

121 # and gathering them into a big per-app list.

122 # Then go through that list, order it, and split into migrations to

123 # resolve dependencies caused by M2Ms and FKs.

124 self.generated_operations = {}

125 self.altered_indexes = {}

126 self.altered_constraints = {}

127 self.renamed_fields = {}

128

129 # Prepare some old/new state and model lists, ignoring unmigrated packages.

130 self.old_model_keys = set()

131 self.old_unmanaged_keys = set()

132 self.new_model_keys = set()

133 self.new_unmanaged_keys = set()

134 for (package_label, model_name), model_state in self.from_state.models.items():

135 if not model_state.options.get("managed", True):

136 self.old_unmanaged_keys.add((package_label, model_name))

137 elif package_label not in self.from_state.real_packages:

138 self.old_model_keys.add((package_label, model_name))

139

140 for (package_label, model_name), model_state in self.to_state.models.items():

141 if not model_state.options.get("managed", True):

142 self.new_unmanaged_keys.add((package_label, model_name))

143 elif package_label not in self.from_state.real_packages or (

144 convert_packages and package_label in convert_packages

145 ):

146 self.new_model_keys.add((package_label, model_name))

147

148 self.from_state.resolve_fields_and_relations()

149 self.to_state.resolve_fields_and_relations()

150

151 # Renames have to come first

152 self.generate_renamed_models()

153

154 # Prepare lists of fields and generate through model map

155 self._prepare_field_lists()

156 self._generate_through_model_map()

157

158 # Generate non-rename model operations

159 self.generate_deleted_models()

160 self.generate_created_models()

161 self.generate_altered_options()

162 self.generate_altered_managers()

163 self.generate_altered_db_table_comment()

164

165 # Create the renamed fields and store them in self.renamed_fields.

166 # They are used by create_altered_indexes(), generate_altered_fields(),

167 # generate_removed_altered_index(), and

168 # generate_altered_index().

169 self.create_renamed_fields()

170 # Create the altered indexes and store them in self.altered_indexes.

171 # This avoids the same computation in generate_removed_indexes()

172 # and generate_added_indexes().

173 self.create_altered_indexes()

174 self.create_altered_constraints()

175 # Generate index removal operations before field is removed

176 self.generate_removed_constraints()

177 self.generate_removed_indexes()

178 # Generate field renaming operations.

179 self.generate_renamed_fields()

180 self.generate_renamed_indexes()

181 # Generate field operations.

182 self.generate_removed_fields()

183 self.generate_added_fields()

184 self.generate_altered_fields()

185 self.generate_altered_order_with_respect_to()

186 self.generate_added_indexes()

187 self.generate_added_constraints()

188 self.generate_altered_db_table()

189

190 self._sort_migrations()

191 self._build_migration_list(graph)

192 self._optimize_migrations()

193

194 return self.migrations

195

196 def _prepare_field_lists(self):

197 """

198 Prepare field lists and a list of the fields that used through models

199 in the old state so dependencies can be made from the through model

200 deletion to the field that uses it.

201 """

202 self.kept_model_keys = self.old_model_keys & self.new_model_keys

203 self.kept_unmanaged_keys = self.old_unmanaged_keys & self.new_unmanaged_keys

204 self.through_users = {}

205 self.old_field_keys = {

206 (package_label, model_name, field_name)

207 for package_label, model_name in self.kept_model_keys

208 for field_name in self.from_state.models[

209 package_label,

210 self.renamed_models.get((package_label, model_name), model_name),

211 ].fields

212 }

213 self.new_field_keys = {

214 (package_label, model_name, field_name)

215 for package_label, model_name in self.kept_model_keys

216 for field_name in self.to_state.models[package_label, model_name].fields

217 }

218

219 def _generate_through_model_map(self):

220 """Through model map generation."""

221 for package_label, model_name in sorted(self.old_model_keys):

222 old_model_name = self.renamed_models.get(

223 (package_label, model_name), model_name

224 )

225 old_model_state = self.from_state.models[package_label, old_model_name]

226 for field_name, field in old_model_state.fields.items():

227 if hasattr(field, "remote_field") and getattr(

228 field.remote_field, "through", None

229 ):

230 through_key = resolve_relation(

231 field.remote_field.through, package_label, model_name

232 )

233 self.through_users[through_key] = (

234 package_label,

235 old_model_name,

236 field_name,

237 )

238

239 @staticmethod

240 def _resolve_dependency(dependency):

241 """

242 Return the resolved dependency and a boolean denoting whether or not

243 it was swappable.

244 """

245 if dependency[0] != "__setting__":

246 return dependency, False

247 resolved_package_label, resolved_object_name = getattr(

248 settings, dependency[1]

249 ).split(".")

250 return (resolved_package_label, resolved_object_name.lower()) + dependency[

251 2:

252 ], True

253

254 def _build_migration_list(self, graph=None):

255 """

256 Chop the lists of operations up into migrations with dependencies on

257 each other. Do this by going through an app's list of operations until

258 one is found that has an outgoing dependency that isn't in another

259 app's migration yet (hasn't been chopped off its list). Then chop off

260 the operations before it into a migration and move onto the next app.

261 If the loops completes without doing anything, there's a circular

262 dependency (which _should_ be impossible as the operations are

263 all split at this point so they can't depend and be depended on).

264 """

265 self.migrations = {}

266 num_ops = sum(len(x) for x in self.generated_operations.values())

267 chop_mode = False

268 while num_ops:

269 # On every iteration, we step through all the packages and see if there

270 # is a completed set of operations.

271 # If we find that a subset of the operations are complete we can

272 # try to chop it off from the rest and continue, but we only

273 # do this if we've already been through the list once before

274 # without any chopping and nothing has changed.

275 for package_label in sorted(self.generated_operations):

276 chopped = []

277 dependencies = set()

278 for operation in list(self.generated_operations[package_label]):

279 deps_satisfied = True

280 operation_dependencies = set()

281 for dep in operation._auto_deps:

282 # Temporarily resolve the swappable dependency to

283 # prevent circular references. While keeping the

284 # dependency checks on the resolved model, add the

285 # swappable dependencies.

286 original_dep = dep

287 dep, is_swappable_dep = self._resolve_dependency(dep)

288 if dep[0] != package_label:

289 # External app dependency. See if it's not yet

290 # satisfied.

291 for other_operation in self.generated_operations.get(

292 dep[0], []

293 ):

294 if self.check_dependency(other_operation, dep):

295 deps_satisfied = False

296 break

297 if not deps_satisfied:

298 break

299 else:

300 if is_swappable_dep:

301 operation_dependencies.add(

302 (original_dep[0], original_dep[1])

303 )

304 elif dep[0] in self.migrations:

305 operation_dependencies.add(

306 (dep[0], self.migrations[dep[0]][-1].name)

307 )

308 else:

309 # If we can't find the other app, we add a

310 # first/last dependency, but only if we've

311 # already been through once and checked

312 # everything.

313 if chop_mode:

314 # If the app already exists, we add a

315 # dependency on the last migration, as

316 # we don't know which migration

317 # contains the target field. If it's

318 # not yet migrated or has no

319 # migrations, we use __first__.

320 if graph and graph.leaf_nodes(dep[0]):

321 operation_dependencies.add(

322 graph.leaf_nodes(dep[0])[0]

323 )

324 else:

325 operation_dependencies.add(

326 (dep[0], "__first__")

327 )

328 else:

329 deps_satisfied = False

330 if deps_satisfied:

331 chopped.append(operation)

332 dependencies.update(operation_dependencies)

333 del self.generated_operations[package_label][0]

334 else:

335 break

336 # Make a migration! Well, only if there's stuff to put in it

337 if dependencies or chopped:

338 if not self.generated_operations[package_label] or chop_mode:

339 subclass = type(

340 "Migration",

341 (Migration,),

342 {"operations": [], "dependencies": []},

343 )

344 instance = subclass(

345 "auto_%i"

346 % (len(self.migrations.get(package_label, [])) + 1),

347 package_label,

348 )

349 instance.dependencies = list(dependencies)

350 instance.operations = chopped

351 instance.initial = package_label not in self.existing_packages

352 self.migrations.setdefault(package_label, []).append(instance)

353 chop_mode = False

354 else:

355 self.generated_operations[package_label] = (

356 chopped + self.generated_operations[package_label]

357 )

358 new_num_ops = sum(len(x) for x in self.generated_operations.values())

359 if new_num_ops == num_ops:

360 if not chop_mode:

361 chop_mode = True

362 else:

363 raise ValueError(

364 "Cannot resolve operation dependencies: %r"

365 % self.generated_operations

366 )

367 num_ops = new_num_ops

368

369 def _sort_migrations(self):

370 """

371 Reorder to make things possible. Reordering may be needed so FKs work

372 nicely inside the same app.

373 """

374 for package_label, ops in sorted(self.generated_operations.items()):

375 ts = TopologicalSorter()

376 for op in ops:

377 ts.add(op)

378 for dep in op._auto_deps:

379 # Resolve intra-app dependencies to handle circular

380 # references involving a swappable model.

381 dep = self._resolve_dependency(dep)[0]

382 if dep[0] != package_label:

383 continue

384 ts.add(op, *(x for x in ops if self.check_dependency(x, dep)))

385 self.generated_operations[package_label] = list(ts.static_order())

386

387 def _optimize_migrations(self):

388 # Add in internal dependencies among the migrations

389 for package_label, migrations in self.migrations.items():

390 for m1, m2 in zip(migrations, migrations[1:]):

391 m2.dependencies.append((package_label, m1.name))

392

393 # De-dupe dependencies

394 for migrations in self.migrations.values():

395 for migration in migrations:

396 migration.dependencies = list(set(migration.dependencies))

397

398 # Optimize migrations

399 for package_label, migrations in self.migrations.items():

400 for migration in migrations:

401 migration.operations = MigrationOptimizer().optimize(

402 migration.operations, package_label

403 )

404

405 def check_dependency(self, operation, dependency):

406 """

407 Return True if the given operation depends on the given dependency,

408 False otherwise.

409 """

410 # Created model

411 if dependency[2] is None and dependency[3] is True:

412 return (

413 isinstance(operation, operations.CreateModel)

414 and operation.name_lower == dependency[1].lower()

415 )

416 # Created field

417 elif dependency[2] is not None and dependency[3] is True:

418 return (

419 isinstance(operation, operations.CreateModel)

420 and operation.name_lower == dependency[1].lower()

421 and any(dependency[2] == x for x, y in operation.fields)

422 ) or (

423 isinstance(operation, operations.AddField)

424 and operation.model_name_lower == dependency[1].lower()

425 and operation.name_lower == dependency[2].lower()

426 )

427 # Removed field

428 elif dependency[2] is not None and dependency[3] is False:

429 return (

430 isinstance(operation, operations.RemoveField)

431 and operation.model_name_lower == dependency[1].lower()

432 and operation.name_lower == dependency[2].lower()

433 )

434 # Removed model

435 elif dependency[2] is None and dependency[3] is False:

436 return (

437 isinstance(operation, operations.DeleteModel)

438 and operation.name_lower == dependency[1].lower()

439 )

440 # Field being altered

441 elif dependency[2] is not None and dependency[3] == "alter":

442 return (

443 isinstance(operation, operations.AlterField)

444 and operation.model_name_lower == dependency[1].lower()

445 and operation.name_lower == dependency[2].lower()

446 )

447 # order_with_respect_to being unset for a field

448 elif dependency[2] is not None and dependency[3] == "order_wrt_unset":

449 return (

450 isinstance(operation, operations.AlterOrderWithRespectTo)

451 and operation.name_lower == dependency[1].lower()

452 and (operation.order_with_respect_to or "").lower()

453 != dependency[2].lower()

454 )

455 # Unknown dependency. Raise an error.

456 else:

457 raise ValueError(f"Can't handle dependency {dependency! r}")

458

459 def add_operation(

460 self, package_label, operation, dependencies=None, beginning=False

461 ):

462 # Dependencies are

463 # (package_label, model_name, field_name, create/delete as True/False)

464 operation._auto_deps = dependencies or []

465 if beginning:

466 self.generated_operations.setdefault(package_label, []).insert(0, operation)

467 else:

468 self.generated_operations.setdefault(package_label, []).append(operation)

469

470 def swappable_first_key(self, item):

471 """

472 Place potential swappable models first in lists of created models (only

473 real way to solve #22783).

474 """

475 try:

476 model_state = self.to_state.models[item]

477 base_names = {

478 base if isinstance(base, str) else base.__name__

479 for base in model_state.bases

480 }

481 string_version = f"{item[0]}.{item[1]}"

482 if (

483 model_state.options.get("swappable")

484 or "BaseUser" in base_names

485 or "AbstractBaseUser" in base_names

486 or settings.AUTH_USER_MODEL.lower() == string_version.lower()

487 ):

488 return ("___" + item[0], "___" + item[1])

489 except LookupError:

490 pass

491 return item

492

493 def generate_renamed_models(self):

494 """

495 Find any renamed models, generate the operations for them, and remove

496 the old entry from the model lists. Must be run before other

497 model-level generation.

498 """

499 self.renamed_models = {}

500 self.renamed_models_rel = {}

501 added_models = self.new_model_keys - self.old_model_keys

502 for package_label, model_name in sorted(added_models):

503 model_state = self.to_state.models[package_label, model_name]

504 model_fields_def = self.only_relation_agnostic_fields(model_state.fields)

505

506 removed_models = self.old_model_keys - self.new_model_keys

507 for rem_package_label, rem_model_name in removed_models:

508 if rem_package_label == package_label:

509 rem_model_state = self.from_state.models[

510 rem_package_label, rem_model_name

511 ]

512 rem_model_fields_def = self.only_relation_agnostic_fields(

513 rem_model_state.fields

514 )

515 if model_fields_def == rem_model_fields_def:

516 if self.questioner.ask_rename_model(

517 rem_model_state, model_state

518 ):

519 dependencies = []

520 fields = list(model_state.fields.values()) + [

521 field.remote_field

522 for relations in self.to_state.relations[

523 package_label, model_name

524 ].values()

525 for field in relations.values()

526 ]

527 for field in fields:

528 if field.is_relation:

529 dependencies.extend(

530 self._get_dependencies_for_foreign_key(

531 package_label,

532 model_name,

533 field,

534 self.to_state,

535 )

536 )

537 self.add_operation(

538 package_label,

539 operations.RenameModel(

540 old_name=rem_model_state.name,

541 new_name=model_state.name,

542 ),

543 dependencies=dependencies,

544 )

545 self.renamed_models[

546 package_label, model_name

547 ] = rem_model_name

548 renamed_models_rel_key = "{}.{}".format(

549 rem_model_state.package_label,

550 rem_model_state.name_lower,

551 )

552 self.renamed_models_rel[

553 renamed_models_rel_key

554 ] = f"{model_state.package_label}.{model_state.name_lower}"

555 self.old_model_keys.remove(

556 (rem_package_label, rem_model_name)

557 )

558 self.old_model_keys.add((package_label, model_name))

559 break

560

561 def generate_created_models(self):

562 """

563 Find all new models (both managed and unmanaged) and make create

564 operations for them as well as separate operations to create any

565 foreign key or M2M relationships (these are optimized later, if

566 possible).

567

568 Defer any model options that refer to collections of fields that might

569 be deferred.

570 """

571 old_keys = self.old_model_keys | self.old_unmanaged_keys

572 added_models = self.new_model_keys - old_keys

573 added_unmanaged_models = self.new_unmanaged_keys - old_keys

574 all_added_models = chain(

575 sorted(added_models, key=self.swappable_first_key, reverse=True),

576 sorted(added_unmanaged_models, key=self.swappable_first_key, reverse=True),

577 )

578 for package_label, model_name in all_added_models:

579 model_state = self.to_state.models[package_label, model_name]

580 # Gather related fields

581 related_fields = {}

582 primary_key_rel = None

583 for field_name, field in model_state.fields.items():

584 if field.remote_field:

585 if field.remote_field.model:

586 if field.primary_key:

587 primary_key_rel = field.remote_field.model

588 elif not field.remote_field.parent_link:

589 related_fields[field_name] = field

590 if getattr(field.remote_field, "through", None):

591 related_fields[field_name] = field

592

593 # Are there indexes to defer?

594 indexes = model_state.options.pop("indexes")

595 constraints = model_state.options.pop("constraints")

596 order_with_respect_to = model_state.options.pop(

597 "order_with_respect_to", None

598 )

599 # Depend on the deletion of any possible proxy version of us

600 dependencies = [

601 (package_label, model_name, None, False),

602 ]

603 # Depend on all bases

604 for base in model_state.bases:

605 if isinstance(base, str) and "." in base:

606 base_package_label, base_name = base.split(".", 1)

607 dependencies.append((base_package_label, base_name, None, True))

608 # Depend on the removal of base fields if the new model has

609 # a field with the same name.

610 old_base_model_state = self.from_state.models.get(

611 (base_package_label, base_name)

612 )

613 new_base_model_state = self.to_state.models.get(

614 (base_package_label, base_name)

615 )

616 if old_base_model_state and new_base_model_state:

617 removed_base_fields = (

618 set(old_base_model_state.fields)

619 .difference(

620 new_base_model_state.fields,

621 )

622 .intersection(model_state.fields)

623 )

624 for removed_base_field in removed_base_fields:

625 dependencies.append(

626 (

627 base_package_label,

628 base_name,

629 removed_base_field,

630 False,

631 )

632 )

633 # Depend on the other end of the primary key if it's a relation

634 if primary_key_rel:

635 dependencies.append(

636 resolve_relation(

637 primary_key_rel,

638 package_label,

639 model_name,

640 )

641 + (None, True)

642 )

643 # Generate creation operation

644 self.add_operation(

645 package_label,

646 operations.CreateModel(

647 name=model_state.name,

648 fields=[

649 d

650 for d in model_state.fields.items()

651 if d[0] not in related_fields

652 ],

653 options=model_state.options,

654 bases=model_state.bases,

655 managers=model_state.managers,

656 ),

657 dependencies=dependencies,

658 beginning=True,

659 )

660

661 # Don't add operations which modify the database for unmanaged models

662 if not model_state.options.get("managed", True):

663 continue

664

665 # Generate operations for each related field

666 for name, field in sorted(related_fields.items()):

667 dependencies = self._get_dependencies_for_foreign_key(

668 package_label,

669 model_name,

670 field,

671 self.to_state,

672 )

673 # Depend on our own model being created

674 dependencies.append((package_label, model_name, None, True))

675 # Make operation

676 self.add_operation(

677 package_label,

678 operations.AddField(

679 model_name=model_name,

680 name=name,

681 field=field,

682 ),

683 dependencies=list(set(dependencies)),

684 )

685 # Generate other opns

686 if order_with_respect_to:

687 self.add_operation(

688 package_label,

689 operations.AlterOrderWithRespectTo(

690 name=model_name,

691 order_with_respect_to=order_with_respect_to,

692 ),

693 dependencies=[

694 (package_label, model_name, order_with_respect_to, True),

695 (package_label, model_name, None, True),

696 ],

697 )

698 related_dependencies = [

699 (package_label, model_name, name, True)

700 for name in sorted(related_fields)

701 ]

702 related_dependencies.append((package_label, model_name, None, True))

703 for index in indexes:

704 self.add_operation(

705 package_label,

706 operations.AddIndex(

707 model_name=model_name,

708 index=index,

709 ),

710 dependencies=related_dependencies,

711 )

712 for constraint in constraints:

713 self.add_operation(

714 package_label,

715 operations.AddConstraint(

716 model_name=model_name,

717 constraint=constraint,

718 ),

719 dependencies=related_dependencies,

720 )

721

722 def generate_deleted_models(self):

723 """

724 Find all deleted models (managed and unmanaged) and make delete

725 operations for them as well as separate operations to delete any

726 foreign key or M2M relationships (these are optimized later, if

727 possible).

728

729 Also bring forward removal of any model options that refer to

730 collections of fields - the inverse of generate_created_models().

731 """

732 new_keys = self.new_model_keys | self.new_unmanaged_keys

733 deleted_models = self.old_model_keys - new_keys

734 deleted_unmanaged_models = self.old_unmanaged_keys - new_keys

735 all_deleted_models = chain(

736 sorted(deleted_models), sorted(deleted_unmanaged_models)

737 )

738 for package_label, model_name in all_deleted_models:

739 model_state = self.from_state.models[package_label, model_name]

740 # Gather related fields

741 related_fields = {}

742 for field_name, field in model_state.fields.items():

743 if field.remote_field:

744 if field.remote_field.model:

745 related_fields[field_name] = field

746 if getattr(field.remote_field, "through", None):

747 related_fields[field_name] = field

748

749 # Then remove each related field

750 for name in sorted(related_fields):

751 self.add_operation(

752 package_label,

753 operations.RemoveField(

754 model_name=model_name,

755 name=name,

756 ),

757 )

758 # Finally, remove the model.

759 # This depends on both the removal/alteration of all incoming fields

760 # and the removal of all its own related fields, and if it's

761 # a through model the field that references it.

762 dependencies = []

763 relations = self.from_state.relations

764 for (

765 related_object_package_label,

766 object_name,

767 ), relation_related_fields in relations[package_label, model_name].items():

768 for field_name, field in relation_related_fields.items():

769 dependencies.append(

770 (related_object_package_label, object_name, field_name, False),

771 )

772 if not field.many_to_many:

773 dependencies.append(

774 (

775 related_object_package_label,

776 object_name,

777 field_name,

778 "alter",

779 ),

780 )

781

782 for name in sorted(related_fields):

783 dependencies.append((package_label, model_name, name, False))

784 # We're referenced in another field's through=

785 through_user = self.through_users.get(

786 (package_label, model_state.name_lower)

787 )

788 if through_user:

789 dependencies.append(

790 (through_user[0], through_user[1], through_user[2], False)

791 )

792 # Finally, make the operation, deduping any dependencies

793 self.add_operation(

794 package_label,

795 operations.DeleteModel(

796 name=model_state.name,

797 ),

798 dependencies=list(set(dependencies)),

799 )

800

801 def create_renamed_fields(self):

802 """Work out renamed fields."""

803 self.renamed_operations = []

804 old_field_keys = self.old_field_keys.copy()

805 for package_label, model_name, field_name in sorted(

806 self.new_field_keys - old_field_keys

807 ):

808 old_model_name = self.renamed_models.get(

809 (package_label, model_name), model_name

810 )

811 old_model_state = self.from_state.models[package_label, old_model_name]

812 new_model_state = self.to_state.models[package_label, model_name]

813 field = new_model_state.get_field(field_name)

814 # Scan to see if this is actually a rename!

815 field_dec = self.deep_deconstruct(field)

816 for rem_package_label, rem_model_name, rem_field_name in sorted(

817 old_field_keys - self.new_field_keys

818 ):

819 if rem_package_label == package_label and rem_model_name == model_name:

820 old_field = old_model_state.get_field(rem_field_name)

821 old_field_dec = self.deep_deconstruct(old_field)

822 if (

823 field.remote_field

824 and field.remote_field.model

825 and "to" in old_field_dec[2]

826 ):

827 old_rel_to = old_field_dec[2]["to"]

828 if old_rel_to in self.renamed_models_rel:

829 old_field_dec[2]["to"] = self.renamed_models_rel[old_rel_to]

830 old_field.set_attributes_from_name(rem_field_name)

831 old_db_column = old_field.get_attname_column()[1]

832 if old_field_dec == field_dec or (

833 # Was the field renamed and db_column equal to the

834 # old field's column added?

835 old_field_dec[0:2] == field_dec[0:2]

836 and dict(old_field_dec[2], db_column=old_db_column)

837 == field_dec[2]

838 ):

839 if self.questioner.ask_rename(

840 model_name, rem_field_name, field_name, field

841 ):

842 self.renamed_operations.append(

843 (

844 rem_package_label,

845 rem_model_name,

846 old_field.db_column,

847 rem_field_name,

848 package_label,

849 model_name,

850 field,

851 field_name,

852 )

853 )

854 old_field_keys.remove(

855 (rem_package_label, rem_model_name, rem_field_name)

856 )

857 old_field_keys.add((package_label, model_name, field_name))

858 self.renamed_fields[

859 package_label, model_name, field_name

860 ] = rem_field_name

861 break

862

863 def generate_renamed_fields(self):

864 """Generate RenameField operations."""

865 for (

866 rem_package_label,

867 rem_model_name,

868 rem_db_column,

869 rem_field_name,

870 package_label,

871 model_name,

872 field,

873 field_name,

874 ) in self.renamed_operations:

875 # A db_column mismatch requires a prior noop AlterField for the

876 # subsequent RenameField to be a noop on attempts at preserving the

877 # old name.

878 if rem_db_column != field.db_column:

879 altered_field = field.clone()

880 altered_field.name = rem_field_name

881 self.add_operation(

882 package_label,

883 operations.AlterField(

884 model_name=model_name,

885 name=rem_field_name,

886 field=altered_field,

887 ),

888 )

889 self.add_operation(

890 package_label,

891 operations.RenameField(

892 model_name=model_name,

893 old_name=rem_field_name,

894 new_name=field_name,

895 ),

896 )

897 self.old_field_keys.remove(

898 (rem_package_label, rem_model_name, rem_field_name)

899 )

900 self.old_field_keys.add((package_label, model_name, field_name))

901

902 def generate_added_fields(self):

903 """Make AddField operations."""

904 for package_label, model_name, field_name in sorted(

905 self.new_field_keys - self.old_field_keys

906 ):

907 self._generate_added_field(package_label, model_name, field_name)

908

909 def _generate_added_field(self, package_label, model_name, field_name):

910 field = self.to_state.models[package_label, model_name].get_field(field_name)

911 # Adding a field always depends at least on its removal.

912 dependencies = [(package_label, model_name, field_name, False)]

913 # Fields that are foreignkeys/m2ms depend on stuff.

914 if field.remote_field and field.remote_field.model:

915 dependencies.extend(

916 self._get_dependencies_for_foreign_key(

917 package_label,

918 model_name,

919 field,

920 self.to_state,

921 )

922 )

923 # You can't just add NOT NULL fields with no default or fields

924 # which don't allow empty strings as default.

925 time_fields = (models.DateField, models.DateTimeField, models.TimeField)

926 preserve_default = (

927 field.null

928 or field.has_default()

929 or field.many_to_many

930 or (field.blank and field.empty_strings_allowed)

931 or (isinstance(field, time_fields) and field.auto_now)

932 )

933 if not preserve_default:

934 field = field.clone()

935 if isinstance(field, time_fields) and field.auto_now_add:

936 field.default = self.questioner.ask_auto_now_add_addition(

937 field_name, model_name

938 )

939 else:

940 field.default = self.questioner.ask_not_null_addition(

941 field_name, model_name

942 )

943 if (

944 field.unique

945 and field.default is not models.NOT_PROVIDED

946 and callable(field.default)

947 ):

948 self.questioner.ask_unique_callable_default_addition(field_name, model_name)

949 self.add_operation(

950 package_label,

951 operations.AddField(

952 model_name=model_name,

953 name=field_name,

954 field=field,

955 preserve_default=preserve_default,

956 ),

957 dependencies=dependencies,

958 )

959

960 def generate_removed_fields(self):

961 """Make RemoveField operations."""

962 for package_label, model_name, field_name in sorted(

963 self.old_field_keys - self.new_field_keys

964 ):

965 self._generate_removed_field(package_label, model_name, field_name)

966

967 def _generate_removed_field(self, package_label, model_name, field_name):

968 self.add_operation(

969 package_label,

970 operations.RemoveField(

971 model_name=model_name,

972 name=field_name,

973 ),

974 # We might need to depend on the removal of an

975 # order_with_respect_to or index operation;

976 # this is safely ignored if there isn't one

977 dependencies=[

978 (package_label, model_name, field_name, "order_wrt_unset"),

979 ],

980 )

981

982 def generate_altered_fields(self):

983 """

984 Make AlterField operations, or possibly RemovedField/AddField if alter

985 isn't possible.

986 """

987 for package_label, model_name, field_name in sorted(

988 self.old_field_keys & self.new_field_keys

989 ):

990 # Did the field change?

991 old_model_name = self.renamed_models.get(

992 (package_label, model_name), model_name

993 )

994 old_field_name = self.renamed_fields.get(

995 (package_label, model_name, field_name), field_name

996 )

997 old_field = self.from_state.models[package_label, old_model_name].get_field(

998 old_field_name

999 )

1000 new_field = self.to_state.models[package_label, model_name].get_field(

1001 field_name

1002 )

1003 dependencies = []

1004 # Implement any model renames on relations; these are handled by RenameModel

1005 # so we need to exclude them from the comparison

1006 if hasattr(new_field, "remote_field") and getattr(

1007 new_field.remote_field, "model", None

1008 ):

1009 rename_key = resolve_relation(

1010 new_field.remote_field.model, package_label, model_name

1011 )

1012 if rename_key in self.renamed_models:

1013 new_field.remote_field.model = old_field.remote_field.model

1014 # Handle ForeignKey which can only have a single to_field.

1015 remote_field_name = getattr(new_field.remote_field, "field_name", None)

1016 if remote_field_name:

1017 to_field_rename_key = rename_key + (remote_field_name,)

1018 if to_field_rename_key in self.renamed_fields:

1019 # Repoint both model and field name because to_field

1020 # inclusion in ForeignKey.deconstruct() is based on

1021 # both.

1022 new_field.remote_field.model = old_field.remote_field.model

1023 new_field.remote_field.field_name = (

1024 old_field.remote_field.field_name

1025 )

1026 # Handle ForeignObjects which can have multiple from_fields/to_fields.

1027 from_fields = getattr(new_field, "from_fields", None)

1028 if from_fields:

1029 from_rename_key = (package_label, model_name)

1030 new_field.from_fields = tuple(

1031 [

1032 self.renamed_fields.get(

1033 from_rename_key + (from_field,), from_field

1034 )

1035 for from_field in from_fields

1036 ]

1037 )

1038 new_field.to_fields = tuple(

1039 [

1040 self.renamed_fields.get(rename_key + (to_field,), to_field)

1041 for to_field in new_field.to_fields

1042 ]

1043 )

1044 dependencies.extend(

1045 self._get_dependencies_for_foreign_key(

1046 package_label,

1047 model_name,

1048 new_field,

1049 self.to_state,

1050 )

1051 )

1052 if hasattr(new_field, "remote_field") and getattr(

1053 new_field.remote_field, "through", None

1054 ):

1055 rename_key = resolve_relation(

1056 new_field.remote_field.through, package_label, model_name

1057 )

1058 if rename_key in self.renamed_models:

1059 new_field.remote_field.through = old_field.remote_field.through

1060 old_field_dec = self.deep_deconstruct(old_field)

1061 new_field_dec = self.deep_deconstruct(new_field)

1062 # If the field was confirmed to be renamed it means that only

1063 # db_column was allowed to change which generate_renamed_fields()

1064 # already accounts for by adding an AlterField operation.

1065 if old_field_dec != new_field_dec and old_field_name == field_name:

1066 both_m2m = old_field.many_to_many and new_field.many_to_many

1067 neither_m2m = not old_field.many_to_many and not new_field.many_to_many

1068 if both_m2m or neither_m2m:

1069 # Either both fields are m2m or neither is

1070 preserve_default = True

1071 if (

1072 old_field.null

1073 and not new_field.null

1074 and not new_field.has_default()

1075 and not new_field.many_to_many

1076 ):

1077 field = new_field.clone()

1078 new_default = self.questioner.ask_not_null_alteration(

1079 field_name, model_name

1080 )

1081 if new_default is not models.NOT_PROVIDED:

1082 field.default = new_default

1083 preserve_default = False

1084 else:

1085 field = new_field

1086 self.add_operation(

1087 package_label,

1088 operations.AlterField(

1089 model_name=model_name,

1090 name=field_name,

1091 field=field,

1092 preserve_default=preserve_default,

1093 ),

1094 dependencies=dependencies,

1095 )

1096 else:

1097 # We cannot alter between m2m and concrete fields

1098 self._generate_removed_field(package_label, model_name, field_name)

1099 self._generate_added_field(package_label, model_name, field_name)

1100

1101 def create_altered_indexes(self):

1102 option_name = operations.AddIndex.option_name

1103

1104 for package_label, model_name in sorted(self.kept_model_keys):

1105 old_model_name = self.renamed_models.get(

1106 (package_label, model_name), model_name

1107 )

1108 old_model_state = self.from_state.models[package_label, old_model_name]

1109 new_model_state = self.to_state.models[package_label, model_name]

1110

1111 old_indexes = old_model_state.options[option_name]

1112 new_indexes = new_model_state.options[option_name]

1113 added_indexes = [idx for idx in new_indexes if idx not in old_indexes]

1114 removed_indexes = [idx for idx in old_indexes if idx not in new_indexes]

1115 renamed_indexes = []

1116 # Find renamed indexes.

1117 remove_from_added = []

1118 remove_from_removed = []

1119 for new_index in added_indexes:

1120 new_index_dec = new_index.deconstruct()

1121 new_index_name = new_index_dec[2].pop("name")

1122 for old_index in removed_indexes:

1123 old_index_dec = old_index.deconstruct()

1124 old_index_name = old_index_dec[2].pop("name")

1125 # Indexes are the same except for the names.

1126 if (

1127 new_index_dec == old_index_dec

1128 and new_index_name != old_index_name

1129 ):

1130 renamed_indexes.append((old_index_name, new_index_name, None))

1131 remove_from_added.append(new_index)

1132 remove_from_removed.append(old_index)

1133

1134 # Remove renamed indexes from the lists of added and removed

1135 # indexes.

1136 added_indexes = [

1137 idx for idx in added_indexes if idx not in remove_from_added

1138 ]

1139 removed_indexes = [

1140 idx for idx in removed_indexes if idx not in remove_from_removed

1141 ]

1142

1143 self.altered_indexes.update(

1144 {

1145 (package_label, model_name): {

1146 "added_indexes": added_indexes,

1147 "removed_indexes": removed_indexes,

1148 "renamed_indexes": renamed_indexes,

1149 }

1150 }

1151 )

1152

1153 def generate_added_indexes(self):

1154 for (package_label, model_name), alt_indexes in self.altered_indexes.items():

1155 dependencies = self._get_dependencies_for_model(package_label, model_name)

1156 for index in alt_indexes["added_indexes"]:

1157 self.add_operation(

1158 package_label,

1159 operations.AddIndex(

1160 model_name=model_name,

1161 index=index,

1162 ),

1163 dependencies=dependencies,

1164 )

1165

1166 def generate_removed_indexes(self):

1167 for (package_label, model_name), alt_indexes in self.altered_indexes.items():

1168 for index in alt_indexes["removed_indexes"]:

1169 self.add_operation(

1170 package_label,

1171 operations.RemoveIndex(

1172 model_name=model_name,

1173 name=index.name,

1174 ),

1175 )

1176

1177 def generate_renamed_indexes(self):

1178 for (package_label, model_name), alt_indexes in self.altered_indexes.items():

1179 for old_index_name, new_index_name, old_fields in alt_indexes[

1180 "renamed_indexes"

1181 ]:

1182 self.add_operation(

1183 package_label,

1184 operations.RenameIndex(

1185 model_name=model_name,

1186 new_name=new_index_name,

1187 old_name=old_index_name,

1188 old_fields=old_fields,

1189 ),

1190 )

1191

1192 def create_altered_constraints(self):

1193 option_name = operations.AddConstraint.option_name

1194 for package_label, model_name in sorted(self.kept_model_keys):

1195 old_model_name = self.renamed_models.get(

1196 (package_label, model_name), model_name

1197 )

1198 old_model_state = self.from_state.models[package_label, old_model_name]

1199 new_model_state = self.to_state.models[package_label, model_name]

1200

1201 old_constraints = old_model_state.options[option_name]

1202 new_constraints = new_model_state.options[option_name]

1203 add_constraints = [c for c in new_constraints if c not in old_constraints]

1204 rem_constraints = [c for c in old_constraints if c not in new_constraints]

1205

1206 self.altered_constraints.update(

1207 {

1208 (package_label, model_name): {

1209 "added_constraints": add_constraints,

1210 "removed_constraints": rem_constraints,

1211 }

1212 }

1213 )

1214

1215 def generate_added_constraints(self):

1216 for (

1217 package_label,

1218 model_name,

1219 ), alt_constraints in self.altered_constraints.items():

1220 dependencies = self._get_dependencies_for_model(package_label, model_name)

1221 for constraint in alt_constraints["added_constraints"]:

1222 self.add_operation(

1223 package_label,

1224 operations.AddConstraint(

1225 model_name=model_name,

1226 constraint=constraint,

1227 ),

1228 dependencies=dependencies,

1229 )

1230

1231 def generate_removed_constraints(self):

1232 for (

1233 package_label,

1234 model_name,

1235 ), alt_constraints in self.altered_constraints.items():

1236 for constraint in alt_constraints["removed_constraints"]:

1237 self.add_operation(

1238 package_label,

1239 operations.RemoveConstraint(

1240 model_name=model_name,

1241 name=constraint.name,

1242 ),

1243 )

1244

1245 @staticmethod

1246 def _get_dependencies_for_foreign_key(

1247 package_label, model_name, field, project_state

1248 ):

1249 remote_field_model = None

1250 if hasattr(field.remote_field, "model"):

1251 remote_field_model = field.remote_field.model

1252 else:

1253 relations = project_state.relations[package_label, model_name]

1254 for (remote_package_label, remote_model_name), fields in relations.items():

1255 if any(

1256 field == related_field.remote_field

1257 for related_field in fields.values()

1258 ):

1259 remote_field_model = f"{remote_package_label}.{remote_model_name}"

1260 break

1261 # Account for FKs to swappable models

1262 swappable_setting = getattr(field, "swappable_setting", None)

1263 if swappable_setting is not None:

1264 dep_package_label = "__setting__"

1265 dep_object_name = swappable_setting

1266 else:

1267 dep_package_label, dep_object_name = resolve_relation(

1268 remote_field_model,

1269 package_label,

1270 model_name,

1271 )

1272 dependencies = [(dep_package_label, dep_object_name, None, True)]

1273 if getattr(field.remote_field, "through", None):

1274 through_package_label, through_object_name = resolve_relation(

1275 field.remote_field.through,

1276 package_label,

1277 model_name,

1278 )

1279 dependencies.append(

1280 (through_package_label, through_object_name, None, True)

1281 )

1282 return dependencies

1283

1284 def _get_dependencies_for_model(self, package_label, model_name):

1285 """Return foreign key dependencies of the given model."""

1286 dependencies = []

1287 model_state = self.to_state.models[package_label, model_name]

1288 for field in model_state.fields.values():

1289 if field.is_relation:

1290 dependencies.extend(

1291 self._get_dependencies_for_foreign_key(

1292 package_label,

1293 model_name,

1294 field,

1295 self.to_state,

1296 )

1297 )

1298 return dependencies

1299

1300 def _get_altered_foo_together_operations(self, option_name):

1301 for package_label, model_name in sorted(self.kept_model_keys):

1302 old_model_name = self.renamed_models.get(

1303 (package_label, model_name), model_name

1304 )

1305 old_model_state = self.from_state.models[package_label, old_model_name]

1306 new_model_state = self.to_state.models[package_label, model_name]

1307

1308 # We run the old version through the field renames to account for those

1309 old_value = old_model_state.options.get(option_name)

1310 old_value = (

1311 {

1312 tuple(

1313 self.renamed_fields.get((package_label, model_name, n), n)

1314 for n in unique

1315 )

1316 for unique in old_value

1317 }

1318 if old_value

1319 else set()

1320 )

1321

1322 new_value = new_model_state.options.get(option_name)

1323 new_value = set(new_value) if new_value else set()

1324

1325 if old_value != new_value:

1326 dependencies = []

1327 for foo_togethers in new_value:

1328 for field_name in foo_togethers:

1329 field = new_model_state.get_field(field_name)

1330 if field.remote_field and field.remote_field.model:

1331 dependencies.extend(

1332 self._get_dependencies_for_foreign_key(

1333 package_label,

1334 model_name,

1335 field,

1336 self.to_state,

1337 )

1338 )

1339 yield (

1340 old_value,

1341 new_value,

1342 package_label,

1343 model_name,

1344 dependencies,

1345 )

1346

1347 def _generate_removed_altered_foo_together(self, operation):

1348 for (

1349 old_value,

1350 new_value,

1351 package_label,

1352 model_name,

1353 dependencies,

1354 ) in self._get_altered_foo_together_operations(operation.option_name):

1355 removal_value = new_value.intersection(old_value)

1356 if removal_value or old_value:

1357 self.add_operation(

1358 package_label,

1359 operation(

1360 name=model_name, **{operation.option_name: removal_value}

1361 ),

1362 dependencies=dependencies,

1363 )

1364

1365 def generate_altered_db_table(self):

1366 models_to_check = self.kept_model_keys.union(self.kept_unmanaged_keys)

1367 for package_label, model_name in sorted(models_to_check):

1368 old_model_name = self.renamed_models.get(

1369 (package_label, model_name), model_name

1370 )

1371 old_model_state = self.from_state.models[package_label, old_model_name]

1372 new_model_state = self.to_state.models[package_label, model_name]

1373 old_db_table_name = old_model_state.options.get("db_table")

1374 new_db_table_name = new_model_state.options.get("db_table")

1375 if old_db_table_name != new_db_table_name:

1376 self.add_operation(

1377 package_label,

1378 operations.AlterModelTable(

1379 name=model_name,

1380 table=new_db_table_name,

1381 ),

1382 )

1383

1384 def generate_altered_db_table_comment(self):

1385 models_to_check = self.kept_model_keys.union(self.kept_unmanaged_keys)

1386 for package_label, model_name in sorted(models_to_check):

1387 old_model_name = self.renamed_models.get(

1388 (package_label, model_name), model_name

1389 )

1390 old_model_state = self.from_state.models[package_label, old_model_name]

1391 new_model_state = self.to_state.models[package_label, model_name]

1392

1393 old_db_table_comment = old_model_state.options.get("db_table_comment")

1394 new_db_table_comment = new_model_state.options.get("db_table_comment")

1395 if old_db_table_comment != new_db_table_comment:

1396 self.add_operation(

1397 package_label,

1398 operations.AlterModelTableComment(

1399 name=model_name,

1400 table_comment=new_db_table_comment,

1401 ),

1402 )

1403

1404 def generate_altered_options(self):

1405 """

1406 Work out if any non-schema-affecting options have changed and make an

1407 operation to represent them in state changes (in case Python code in

1408 migrations needs them).

1409 """

1410 models_to_check = self.kept_model_keys.union(

1411 self.kept_unmanaged_keys,

1412 # unmanaged converted to managed

1413 self.old_unmanaged_keys & self.new_model_keys,

1414 # managed converted to unmanaged

1415 self.old_model_keys & self.new_unmanaged_keys,

1416 )

1417

1418 for package_label, model_name in sorted(models_to_check):

1419 old_model_name = self.renamed_models.get(

1420 (package_label, model_name), model_name

1421 )

1422 old_model_state = self.from_state.models[package_label, old_model_name]

1423 new_model_state = self.to_state.models[package_label, model_name]

1424 old_options = {

1425 key: value

1426 for key, value in old_model_state.options.items()

1427 if key in AlterModelOptions.ALTER_OPTION_KEYS

1428 }

1429 new_options = {

1430 key: value

1431 for key, value in new_model_state.options.items()

1432 if key in AlterModelOptions.ALTER_OPTION_KEYS

1433 }

1434 if old_options != new_options:

1435 self.add_operation(

1436 package_label,

1437 operations.AlterModelOptions(

1438 name=model_name,

1439 options=new_options,

1440 ),

1441 )

1442

1443 def generate_altered_order_with_respect_to(self):

1444 for package_label, model_name in sorted(self.kept_model_keys):

1445 old_model_name = self.renamed_models.get(

1446 (package_label, model_name), model_name

1447 )

1448 old_model_state = self.from_state.models[package_label, old_model_name]

1449 new_model_state = self.to_state.models[package_label, model_name]

1450 if old_model_state.options.get(

1451 "order_with_respect_to"

1452 ) != new_model_state.options.get("order_with_respect_to"):

1453 # Make sure it comes second if we're adding

1454 # (removal dependency is part of RemoveField)

1455 dependencies = []

1456 if new_model_state.options.get("order_with_respect_to"):

1457 dependencies.append(

1458 (

1459 package_label,

1460 model_name,

1461 new_model_state.options["order_with_respect_to"],

1462 True,

1463 )

1464 )

1465 # Actually generate the operation

1466 self.add_operation(

1467 package_label,

1468 operations.AlterOrderWithRespectTo(

1469 name=model_name,

1470 order_with_respect_to=new_model_state.options.get(

1471 "order_with_respect_to"

1472 ),

1473 ),

1474 dependencies=dependencies,

1475 )

1476

1477 def generate_altered_managers(self):

1478 for package_label, model_name in sorted(self.kept_model_keys):

1479 old_model_name = self.renamed_models.get(

1480 (package_label, model_name), model_name

1481 )

1482 old_model_state = self.from_state.models[package_label, old_model_name]

1483 new_model_state = self.to_state.models[package_label, model_name]

1484 if old_model_state.managers != new_model_state.managers:

1485 self.add_operation(

1486 package_label,

1487 operations.AlterModelManagers(

1488 name=model_name,

1489 managers=new_model_state.managers,

1490 ),

1491 )

1492

1493 def arrange_for_graph(self, changes, graph, migration_name=None):

1494 """

1495 Take a result from changes() and a MigrationGraph, and fix the names

1496 and dependencies of the changes so they extend the graph from the leaf

1497 nodes for each app.

1498 """

1499 leaves = graph.leaf_nodes()

1500 name_map = {}

1501 for package_label, migrations in list(changes.items()):

1502 if not migrations:

1503 continue

1504 # Find the app label's current leaf node

1505 app_leaf = None

1506 for leaf in leaves:

1507 if leaf[0] == package_label:

1508 app_leaf = leaf

1509 break

1510 # Do they want an initial migration for this app?

1511 if app_leaf is None and not self.questioner.ask_initial(package_label):

1512 # They don't.

1513 for migration in migrations:

1514 name_map[(package_label, migration.name)] = (

1515 package_label,

1516 "__first__",

1517 )

1518 del changes[package_label]

1519 continue

1520 # Work out the next number in the sequence

1521 if app_leaf is None:

1522 next_number = 1

1523 else:

1524 next_number = (self.parse_number(app_leaf[1]) or 0) + 1

1525 # Name each migration

1526 for i, migration in enumerate(migrations):

1527 if i == 0 and app_leaf:

1528 migration.dependencies.append(app_leaf)

1529 new_name_parts = ["%04i" % next_number]

1530 if migration_name:

1531 new_name_parts.append(migration_name)

1532 elif i == 0 and not app_leaf:

1533 new_name_parts.append("initial")

1534 else:

1535 new_name_parts.append(migration.suggest_name()[:100])

1536 new_name = "_".join(new_name_parts)

1537 name_map[(package_label, migration.name)] = (package_label, new_name)

1538 next_number += 1

1539 migration.name = new_name

1540 # Now fix dependencies

1541 for migrations in changes.values():

1542 for migration in migrations:

1543 migration.dependencies = [

1544 name_map.get(d, d) for d in migration.dependencies

1545 ]

1546 return changes

1547

1548 def _trim_to_packages(self, changes, package_labels):

1549 """

1550 Take changes from arrange_for_graph() and set of app labels, and return

1551 a modified set of changes which trims out as many migrations that are

1552 not in package_labels as possible. Note that some other migrations may

1553 still be present as they may be required dependencies.

1554 """

1555 # Gather other app dependencies in a first pass

1556 app_dependencies = {}

1557 for package_label, migrations in changes.items():

1558 for migration in migrations:

1559 for dep_package_label, name in migration.dependencies:

1560 app_dependencies.setdefault(package_label, set()).add(

1561 dep_package_label

1562 )

1563 required_packages = set(package_labels)

1564 # Keep resolving till there's no change

1565 old_required_packages = None

1566 while old_required_packages != required_packages:

1567 old_required_packages = set(required_packages)

1568 required_packages.update(

1569 *[

1570 app_dependencies.get(package_label, ())

1571 for package_label in required_packages

1572 ]

1573 )

1574 # Remove all migrations that aren't needed

1575 for package_label in list(changes):

1576 if package_label not in required_packages:

1577 del changes[package_label]

1578 return changes

1579

1580 @classmethod

1581 def parse_number(cls, name):

1582 """

1583 Given a migration name, try to extract a number from the beginning of

1584 it. For a squashed migration such as '0001_squashed_0004…', return the

1585 second number. If no number is found, return None.

1586 """

1587 if squashed_match := re.search(r".*_squashed_(\d+)", name):

1588 return int(squashed_match[1])

1589 match = re.match(r"^\d+", name)

1590 if match:

1591 return int(match[0])

1592 return None