From 1282b5e4207440af659ef0e0e0c486fdfba8e7b7 Mon Sep 17 00:00:00 2001 From: Nick Pope Date: Mon, 8 Mar 2021 11:39:56 +0000 Subject: [PATCH] Fixed #32528 -- Replaced django.utils.topological_sort with graphlib.TopologicalSort(). graphlib.TopologicalSort() is available since Python 3.9. --- django/db/migrations/autodetector.py | 14 +++----- django/forms/widgets.py | 12 +++---- django/utils/topological_sort.py | 42 ---------------------- tests/migrations/test_autodetector.py | 4 +-- tests/utils_tests/test_topological_sort.py | 30 ---------------- 5 files changed, 13 insertions(+), 89 deletions(-) delete mode 100644 django/utils/topological_sort.py delete mode 100644 tests/utils_tests/test_topological_sort.py diff --git a/django/db/migrations/autodetector.py b/django/db/migrations/autodetector.py index bb266cac9c..1cb3a19f64 100644 --- a/django/db/migrations/autodetector.py +++ b/django/db/migrations/autodetector.py @@ -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 diff --git a/django/forms/widgets.py b/django/forms/widgets.py index c5c531ada7..a1c67bb863 100644 --- a/django/forms/widgets.py +++ b/django/forms/widgets.py @@ -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) diff --git a/django/utils/topological_sort.py b/django/utils/topological_sort.py deleted file mode 100644 index 66b6866ec8..0000000000 --- a/django/utils/topological_sort.py +++ /dev/null @@ -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 diff --git a/tests/migrations/test_autodetector.py b/tests/migrations/test_autodetector.py index 82ddb17543..6422c82214 100644 --- a/tests/migrations/test_autodetector.py +++ b/tests/migrations/test_autodetector.py @@ -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")] ) diff --git a/tests/utils_tests/test_topological_sort.py b/tests/utils_tests/test_topological_sort.py deleted file mode 100644 index 917cfd5d4a..0000000000 --- a/tests/utils_tests/test_topological_sort.py +++ /dev/null @@ -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}}))