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

This commit is contained in:
Barnaby Gray 2014-02-12 09:09:55 +00:00
parent 7faba11245
commit 6431d52a2e
2 changed files with 67 additions and 23 deletions

View File

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

View File

@ -44,6 +44,27 @@ class SortServiceTest(unittest.TestCase):
self.assertEqual(sorted_services[1]['name'], 'postgres') self.assertEqual(sorted_services[1]['name'], 'postgres')
self.assertEqual(sorted_services[2]['name'], 'web') 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): def test_sort_service_dicts_circular_imports(self):
services = [ services = [
{ {
@ -87,6 +108,30 @@ class SortServiceTest(unittest.TestCase):
else: else:
self.fail('Should have thrown an DependencyError') 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): def test_sort_service_dicts_self_imports(self):
services = [ services = [
{ {