1
0
mirror of https://github.com/django/django.git synced 2025-03-06 07:22:32 +00:00

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

View File

@ -6,6 +6,7 @@ import copy
import datetime import datetime
import warnings import warnings
from collections import defaultdict from collections import defaultdict
from graphlib import CycleError, TopologicalSorter
from itertools import chain from itertools import chain
from django.forms.utils import to_current_timezone 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.html import format_html, html_safe
from django.utils.regex_helper import _lazy_re_compile from django.utils.regex_helper import _lazy_re_compile
from django.utils.safestring import mark_safe 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 django.utils.translation import gettext_lazy as _
from .renderers import get_default_renderer 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 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. global or in CSS you might want to override a style.
""" """
dependency_graph = defaultdict(set) ts = TopologicalSorter()
all_items = OrderedSet() all_items = OrderedSet()
for list_ in filter(None, lists): for list_ in filter(None, lists):
head = list_[0] head = list_[0]
# The first items depend on nothing but have to be part of the # The first items depend on nothing but have to be part of the
# dependency graph to be included in the result. # dependency graph to be included in the result.
dependency_graph.setdefault(head, set()) ts.add(head)
for item in list_: for item in list_:
all_items.add(item) all_items.add(item)
# No self dependencies # No self dependencies
if head != item: if head != item:
dependency_graph[item].add(head) ts.add(item, head)
head = item head = item
try: try:
return stable_topological_sort(all_items, dependency_graph) return list(ts.static_order())
except CyclicDependencyError: except CycleError:
warnings.warn( warnings.warn(
"Detected duplicate Media files in an opposite order: {}".format( "Detected duplicate Media files in an opposite order: {}".format(
", ".join(repr(list_) for list_ in lists) ", ".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? # Right number/type of migrations?
self.assertNumberMigrations(changes, "testapp", 1) self.assertNumberMigrations(changes, "testapp", 1)
self.assertOperationTypes(changes, "testapp", 0, ["CreateModel", "CreateModel"]) self.assertOperationTypes(changes, "testapp", 0, ["CreateModel", "CreateModel"])
self.assertOperationAttributes(changes, "testapp", 0, 0, name="Publisher") self.assertOperationAttributes(changes, "testapp", 0, 0, name="Author")
self.assertOperationAttributes(changes, "testapp", 0, 1, name="Author") self.assertOperationAttributes(changes, "testapp", 0, 1, name="Publisher")
self.assertMigrationDependencies( self.assertMigrationDependencies(
changes, "testapp", 0, [("otherapp", "auto_1")] 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}}))