Fixed #32528 -- Replaced django.utils.topological_sort with graphlib.TopologicalSort().

graphlib.TopologicalSort() is available since Python 3.9.
This commit is contained in:
Nick Pope 2021-03-08 11:39:56 +00:00 committed by Mariusz Felisiak
parent 4470c2405c
commit 1282b5e420
5 changed files with 13 additions and 89 deletions

View File

@ -1,6 +1,7 @@
import functools
import re
from collections import defaultdict
from graphlib import TopologicalSorter
from itertools import chain
from django.conf import settings
@ -15,7 +16,6 @@ from django.db.migrations.utils import (
RegexObject,
resolve_relation,
)
from django.utils.topological_sort import stable_topological_sort
class MigrationAutodetector:
@ -384,9 +384,9 @@ class MigrationAutodetector:
nicely inside the same app.
"""
for app_label, ops in sorted(self.generated_operations.items()):
# construct a dependency graph for intra-app dependencies
dependency_graph = {op: set() for op in ops}
ts = TopologicalSorter()
for op in ops:
ts.add(op)
for dep in op._auto_deps:
# Resolve intra-app dependencies to handle circular
# references involving a swappable model.
@ -394,12 +394,8 @@ class MigrationAutodetector:
if dep[0] == app_label:
for op2 in ops:
if self.check_dependency(op2, dep):
dependency_graph[op].add(op2)
# we use a stable sort for deterministic tests & general behavior
self.generated_operations[app_label] = stable_topological_sort(
ops, dependency_graph
)
ts.add(op, op2)
self.generated_operations[app_label] = list(ts.static_order())
def _optimize_migrations(self):
# Add in internal dependencies among the migrations

View File

@ -6,6 +6,7 @@ import copy
import datetime
import warnings
from collections import defaultdict
from graphlib import CycleError, TopologicalSorter
from itertools import chain
from django.forms.utils import to_current_timezone
@ -17,7 +18,6 @@ from django.utils.formats import get_format
from django.utils.html import format_html, html_safe
from django.utils.regex_helper import _lazy_re_compile
from django.utils.safestring import mark_safe
from django.utils.topological_sort import CyclicDependencyError, stable_topological_sort
from django.utils.translation import gettext_lazy as _
from .renderers import get_default_renderer
@ -151,22 +151,22 @@ class Media:
in a certain order. In JavaScript you may not be able to reference a
global or in CSS you might want to override a style.
"""
dependency_graph = defaultdict(set)
ts = TopologicalSorter()
all_items = OrderedSet()
for list_ in filter(None, lists):
head = list_[0]
# The first items depend on nothing but have to be part of the
# dependency graph to be included in the result.
dependency_graph.setdefault(head, set())
ts.add(head)
for item in list_:
all_items.add(item)
# No self dependencies
if head != item:
dependency_graph[item].add(head)
ts.add(item, head)
head = item
try:
return stable_topological_sort(all_items, dependency_graph)
except CyclicDependencyError:
return list(ts.static_order())
except CycleError:
warnings.warn(
"Detected duplicate Media files in an opposite order: {}".format(
", ".join(repr(list_) for list_ in lists)

View File

@ -1,42 +0,0 @@
class CyclicDependencyError(ValueError):
pass
def topological_sort_as_sets(dependency_graph):
"""
Variation of Kahn's algorithm (1962) that returns sets.
Take a dependency graph as a dictionary of node => dependencies.
Yield sets of items in topological order, where the first set contains
all nodes without dependencies, and each following set contains all
nodes that may depend on the nodes only in the previously yielded sets.
"""
todo = dependency_graph.copy()
while todo:
current = {node for node, deps in todo.items() if not deps}
if not current:
raise CyclicDependencyError(
"Cyclic dependency in graph: {}".format(
", ".join(repr(x) for x in todo.items())
)
)
yield current
# remove current from todo's nodes & dependencies
todo = {
node: (dependencies - current)
for node, dependencies in todo.items()
if node not in current
}
def stable_topological_sort(nodes, dependency_graph):
result = []
for layer in topological_sort_as_sets(dependency_graph):
for node in nodes:
if node in layer:
result.append(node)
return result

View File

@ -2218,8 +2218,8 @@ class AutodetectorTests(BaseAutodetectorTests):
# Right number/type of migrations?
self.assertNumberMigrations(changes, "testapp", 1)
self.assertOperationTypes(changes, "testapp", 0, ["CreateModel", "CreateModel"])
self.assertOperationAttributes(changes, "testapp", 0, 0, name="Publisher")
self.assertOperationAttributes(changes, "testapp", 0, 1, name="Author")
self.assertOperationAttributes(changes, "testapp", 0, 0, name="Author")
self.assertOperationAttributes(changes, "testapp", 0, 1, name="Publisher")
self.assertMigrationDependencies(
changes, "testapp", 0, [("otherapp", "auto_1")]
)

View File

@ -1,30 +0,0 @@
from django.test import SimpleTestCase
from django.utils.topological_sort import (
CyclicDependencyError,
stable_topological_sort,
topological_sort_as_sets,
)
class TopologicalSortTests(SimpleTestCase):
def test_basic(self):
dependency_graph = {
1: {2, 3},
2: set(),
3: set(),
4: {5, 6},
5: set(),
6: {5},
}
self.assertEqual(
list(topological_sort_as_sets(dependency_graph)), [{2, 3, 5}, {1, 6}, {4}]
)
self.assertEqual(
stable_topological_sort([1, 2, 3, 4, 5, 6], dependency_graph),
[2, 3, 5, 1, 6, 4],
)
def test_cyclic_dependency(self):
msg = "Cyclic dependency in graph: (1, {2}), (2, {1})"
with self.assertRaisesMessage(CyclicDependencyError, msg):
list(topological_sort_as_sets({1: {2}, 2: {1}}))