Selaa lähdekoodia

Implement topological sort using Cormen/Tarjan algorithm to handle a->b->c dependencies and detect a->b->c->a cycles.

Barnaby Gray 11 vuotta sitten
vanhempi
sitoutus
6431d52a2e
2 muutettua tiedostoa jossa 67 lisäystä ja 23 poistoa
  1. 22 23
      fig/project.py
  2. 45 0
      tests/sort_service_test.py

+ 22 - 23
fig/project.py

@@ -7,31 +7,30 @@ log = logging.getLogger(__name__)
 
 
 def sort_service_dicts(services):
-    # Get all services that are dependant on another.
-    dependent_services = [s for s in services if s.get('links')]
-    flatten_links = sum([s['links'] for s in dependent_services], [])
-    # Get all services that are not linked to and don't link to others.
-    non_dependent_sevices = [s for s in services if s['name'] not in flatten_links and not s.get('links')]
+    # Topological sort (Cormen/Tarjan algorithm).
+    unmarked = services[:]
+    temporary_marked = set()
     sorted_services = []
-    # Topological sort.
-    while dependent_services:
-        n = dependent_services.pop()
-        # Check if a service is dependent on itself, if so raise an error.
-        if n['name'] in n.get('links', []):
-            raise DependencyError('A service can not link to itself: %s' % n['name'])
-        sorted_services.append(n)
-        for l in n['links']:
-            # Get the linked service.
-            linked_service = next(s for s in services if l == s['name'])
-            # Check that there isn't a circular import between services.
-            if n['name'] in linked_service.get('links', []):
-                raise DependencyError('Circular import between %s and %s' % (n['name'], linked_service['name']))
-            # Check the linked service has no links and is not already in the
-            # sorted service list.
-            if not linked_service.get('links') and linked_service not in sorted_services:
-                sorted_services.insert(0, linked_service)
-    return non_dependent_sevices + sorted_services
 
+    def visit(n):
+        if n['name'] in temporary_marked:
+            if n['name'] in n.get('links', []):
+                raise DependencyError('A service can not link to itself: %s' % n['name'])
+            else:
+                raise DependencyError('Circular import between %s' % ' and '.join(temporary_marked))
+        if n in unmarked:
+            temporary_marked.add(n['name'])
+            dependents = [m for m in services if n['name'] in m.get('links', [])]
+            for m in dependents:
+                visit(m)
+            temporary_marked.remove(n['name'])
+            unmarked.remove(n)
+            sorted_services.insert(0, n)
+
+    while unmarked:
+        visit(unmarked[-1])
+
+    return sorted_services
 
 class Project(object):
     """

+ 45 - 0
tests/sort_service_test.py

@@ -44,6 +44,27 @@ class SortServiceTest(unittest.TestCase):
         self.assertEqual(sorted_services[1]['name'], 'postgres')
         self.assertEqual(sorted_services[2]['name'], 'web')
 
+    def test_sort_service_dicts_3(self):
+        services = [
+            {
+                'name': 'child'
+            },
+            {
+                'name': 'parent',
+                'links': ['child']
+            },
+            {
+                'links': ['parent'],
+                'name': 'grandparent'
+            },
+        ]
+
+        sorted_services = sort_service_dicts(services)
+        self.assertEqual(len(sorted_services), 3)
+        self.assertEqual(sorted_services[0]['name'], 'child')
+        self.assertEqual(sorted_services[1]['name'], 'parent')
+        self.assertEqual(sorted_services[2]['name'], 'grandparent')
+
     def test_sort_service_dicts_circular_imports(self):
         services = [
             {
@@ -87,6 +108,30 @@ class SortServiceTest(unittest.TestCase):
         else:
             self.fail('Should have thrown an DependencyError')
 
+    def test_sort_service_dicts_circular_imports_3(self):
+        services = [
+            {
+                'links': ['b'],
+                'name': 'a'
+            },
+            {
+                'name': 'b',
+                'links': ['c']
+            },
+            {
+                'name': 'c',
+                'links': ['a']
+            }
+        ]
+
+        try:
+            sort_service_dicts(services)
+        except DependencyError as e:
+            self.assertIn('a', e.msg)
+            self.assertIn('b', e.msg)
+        else:
+            self.fail('Should have thrown an DependencyError')
+
     def test_sort_service_dicts_self_imports(self):
         services = [
             {