Merge "Add a directed graph type (new types module)"
This commit is contained in:
@@ -17,16 +17,8 @@ The following classes and modules are *recommended* for external usage:
|
|||||||
.. autoclass:: taskflow.utils.eventlet_utils.GreenExecutor
|
.. autoclass:: taskflow.utils.eventlet_utils.GreenExecutor
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
.. autofunction:: taskflow.utils.graph_utils.pformat
|
|
||||||
|
|
||||||
.. autofunction:: taskflow.utils.graph_utils.export_graph_to_dot
|
|
||||||
|
|
||||||
.. autofunction:: taskflow.utils.persistence_utils.temporary_log_book
|
.. autofunction:: taskflow.utils.persistence_utils.temporary_log_book
|
||||||
|
|
||||||
.. autofunction:: taskflow.utils.persistence_utils.temporary_flow_detail
|
.. autofunction:: taskflow.utils.persistence_utils.temporary_flow_detail
|
||||||
|
|
||||||
.. autofunction:: taskflow.utils.persistence_utils.pformat
|
.. autofunction:: taskflow.utils.persistence_utils.pformat
|
||||||
|
|
||||||
.. autofunction:: taskflow.utils.persistence_utils.pformat_flow_detail
|
|
||||||
|
|
||||||
.. autofunction:: taskflow.utils.persistence_utils.pformat_atom_detail
|
|
||||||
|
|||||||
@@ -16,12 +16,11 @@
|
|||||||
|
|
||||||
import collections
|
import collections
|
||||||
|
|
||||||
import networkx as nx
|
|
||||||
from networkx.algorithms import traversal
|
from networkx.algorithms import traversal
|
||||||
|
|
||||||
from taskflow import exceptions as exc
|
from taskflow import exceptions as exc
|
||||||
from taskflow import flow
|
from taskflow import flow
|
||||||
from taskflow.utils import graph_utils
|
from taskflow.types import graph as gr
|
||||||
|
|
||||||
|
|
||||||
class Flow(flow.Flow):
|
class Flow(flow.Flow):
|
||||||
@@ -39,7 +38,8 @@ class Flow(flow.Flow):
|
|||||||
|
|
||||||
def __init__(self, name, retry=None):
|
def __init__(self, name, retry=None):
|
||||||
super(Flow, self).__init__(name, retry)
|
super(Flow, self).__init__(name, retry)
|
||||||
self._graph = nx.freeze(nx.DiGraph())
|
self._graph = gr.DiGraph()
|
||||||
|
self._graph.freeze()
|
||||||
|
|
||||||
def link(self, u, v):
|
def link(self, u, v):
|
||||||
"""Link existing node u as a runtime dependency of existing node v."""
|
"""Link existing node u as a runtime dependency of existing node v."""
|
||||||
@@ -57,7 +57,7 @@ class Flow(flow.Flow):
|
|||||||
mutable_graph = False
|
mutable_graph = False
|
||||||
# NOTE(harlowja): Add an edge to a temporary copy and only if that
|
# NOTE(harlowja): Add an edge to a temporary copy and only if that
|
||||||
# copy is valid then do we swap with the underlying graph.
|
# copy is valid then do we swap with the underlying graph.
|
||||||
attrs = graph_utils.get_edge_attrs(graph, u, v)
|
attrs = graph.get_edge_data(u, v)
|
||||||
if not attrs:
|
if not attrs:
|
||||||
attrs = {}
|
attrs = {}
|
||||||
if manual:
|
if manual:
|
||||||
@@ -67,21 +67,22 @@ class Flow(flow.Flow):
|
|||||||
attrs['reasons'] = set()
|
attrs['reasons'] = set()
|
||||||
attrs['reasons'].add(reason)
|
attrs['reasons'].add(reason)
|
||||||
if not mutable_graph:
|
if not mutable_graph:
|
||||||
graph = nx.DiGraph(graph)
|
graph = gr.DiGraph(graph)
|
||||||
graph.add_edge(u, v, **attrs)
|
graph.add_edge(u, v, **attrs)
|
||||||
return graph
|
return graph
|
||||||
|
|
||||||
def _swap(self, replacement_graph):
|
def _swap(self, graph):
|
||||||
"""Validates the replacement graph and then swaps the underlying graph
|
"""Validates the replacement graph and then swaps the underlying graph
|
||||||
with a frozen version of the replacement graph (this maintains the
|
with a frozen version of the replacement graph (this maintains the
|
||||||
invariant that the underlying graph is immutable).
|
invariant that the underlying graph is immutable).
|
||||||
"""
|
"""
|
||||||
if not nx.is_directed_acyclic_graph(replacement_graph):
|
if not graph.is_directed_acyclic():
|
||||||
raise exc.DependencyFailure("No path through the items in the"
|
raise exc.DependencyFailure("No path through the items in the"
|
||||||
" graph produces an ordering that"
|
" graph produces an ordering that"
|
||||||
" will allow for correct dependency"
|
" will allow for correct dependency"
|
||||||
" resolution")
|
" resolution")
|
||||||
self._graph = nx.freeze(replacement_graph)
|
self._graph = graph
|
||||||
|
self._graph.freeze()
|
||||||
|
|
||||||
def add(self, *items):
|
def add(self, *items):
|
||||||
"""Adds a given task/tasks/flow/flows to this flow."""
|
"""Adds a given task/tasks/flow/flows to this flow."""
|
||||||
@@ -109,7 +110,7 @@ class Flow(flow.Flow):
|
|||||||
# NOTE(harlowja): Add items and edges to a temporary copy of the
|
# NOTE(harlowja): Add items and edges to a temporary copy of the
|
||||||
# underlying graph and only if that is successful added to do we then
|
# underlying graph and only if that is successful added to do we then
|
||||||
# swap with the underlying graph.
|
# swap with the underlying graph.
|
||||||
tmp_graph = nx.DiGraph(self._graph)
|
tmp_graph = gr.DiGraph(self._graph)
|
||||||
for item in items:
|
for item in items:
|
||||||
tmp_graph.add_node(item)
|
tmp_graph.add_node(item)
|
||||||
update_requirements(item)
|
update_requirements(item)
|
||||||
@@ -237,5 +238,6 @@ class TargetedFlow(Flow):
|
|||||||
nodes = [self._target]
|
nodes = [self._target]
|
||||||
nodes.extend(dst for _src, dst in
|
nodes.extend(dst for _src, dst in
|
||||||
traversal.dfs_edges(self._graph.reverse(), self._target))
|
traversal.dfs_edges(self._graph.reverse(), self._target))
|
||||||
self._subgraph = nx.freeze(self._graph.subgraph(nodes))
|
self._subgraph = self._graph.subgraph(nodes)
|
||||||
|
self._subgraph.freeze()
|
||||||
return self._subgraph
|
return self._subgraph
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
import networkx
|
|
||||||
import testtools
|
import testtools
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
@@ -36,6 +35,7 @@ from taskflow import states
|
|||||||
from taskflow import task
|
from taskflow import task
|
||||||
from taskflow import test
|
from taskflow import test
|
||||||
from taskflow.tests import utils
|
from taskflow.tests import utils
|
||||||
|
from taskflow.types import graph as gr
|
||||||
|
|
||||||
from taskflow.utils import eventlet_utils as eu
|
from taskflow.utils import eventlet_utils as eu
|
||||||
from taskflow.utils import misc
|
from taskflow.utils import misc
|
||||||
@@ -466,7 +466,7 @@ class EngineGraphFlowTest(utils.EngineTestBase):
|
|||||||
engine = self._make_engine(flow)
|
engine = self._make_engine(flow)
|
||||||
engine.compile()
|
engine.compile()
|
||||||
graph = engine.execution_graph
|
graph = engine.execution_graph
|
||||||
self.assertIsInstance(graph, networkx.DiGraph)
|
self.assertIsInstance(graph, gr.DiGraph)
|
||||||
|
|
||||||
def test_task_graph_property_for_one_task(self):
|
def test_task_graph_property_for_one_task(self):
|
||||||
flow = utils.TaskNoRequiresNoReturns(name='task1')
|
flow = utils.TaskNoRequiresNoReturns(name='task1')
|
||||||
@@ -474,7 +474,7 @@ class EngineGraphFlowTest(utils.EngineTestBase):
|
|||||||
engine = self._make_engine(flow)
|
engine = self._make_engine(flow)
|
||||||
engine.compile()
|
engine.compile()
|
||||||
graph = engine.execution_graph
|
graph = engine.execution_graph
|
||||||
self.assertIsInstance(graph, networkx.DiGraph)
|
self.assertIsInstance(graph, gr.DiGraph)
|
||||||
|
|
||||||
|
|
||||||
class EngineCheckingTaskTest(utils.EngineTestBase):
|
class EngineCheckingTaskTest(utils.EngineTestBase):
|
||||||
|
|||||||
@@ -16,8 +16,6 @@
|
|||||||
|
|
||||||
import string
|
import string
|
||||||
|
|
||||||
import networkx as nx
|
|
||||||
|
|
||||||
from taskflow import exceptions as exc
|
from taskflow import exceptions as exc
|
||||||
from taskflow.patterns import graph_flow as gf
|
from taskflow.patterns import graph_flow as gf
|
||||||
from taskflow.patterns import linear_flow as lf
|
from taskflow.patterns import linear_flow as lf
|
||||||
@@ -27,7 +25,6 @@ from taskflow import retry
|
|||||||
from taskflow import test
|
from taskflow import test
|
||||||
from taskflow.tests import utils as t_utils
|
from taskflow.tests import utils as t_utils
|
||||||
from taskflow.utils import flow_utils as f_utils
|
from taskflow.utils import flow_utils as f_utils
|
||||||
from taskflow.utils import graph_utils as g_utils
|
|
||||||
|
|
||||||
|
|
||||||
def _make_many(amount):
|
def _make_many(amount):
|
||||||
@@ -66,13 +63,13 @@ class FlattenTest(test.TestCase):
|
|||||||
g = f_utils.flatten(flo)
|
g = f_utils.flatten(flo)
|
||||||
self.assertEqual(4, len(g))
|
self.assertEqual(4, len(g))
|
||||||
|
|
||||||
order = nx.topological_sort(g)
|
order = g.topological_sort()
|
||||||
self.assertEqual([a, b, c, d], order)
|
self.assertEqual([a, b, c, d], order)
|
||||||
self.assertTrue(g.has_edge(c, d))
|
self.assertTrue(g.has_edge(c, d))
|
||||||
self.assertEqual(g.get_edge_data(c, d), {'invariant': True})
|
self.assertEqual(g.get_edge_data(c, d), {'invariant': True})
|
||||||
|
|
||||||
self.assertEqual([d], list(g_utils.get_no_successors(g)))
|
self.assertEqual([d], list(g.no_successors_iter()))
|
||||||
self.assertEqual([a], list(g_utils.get_no_predecessors(g)))
|
self.assertEqual([a], list(g.no_predecessors_iter()))
|
||||||
|
|
||||||
def test_invalid_flatten(self):
|
def test_invalid_flatten(self):
|
||||||
a, b, c = _make_many(3)
|
a, b, c = _make_many(3)
|
||||||
@@ -89,9 +86,9 @@ class FlattenTest(test.TestCase):
|
|||||||
self.assertEqual(4, len(g))
|
self.assertEqual(4, len(g))
|
||||||
self.assertEqual(0, g.number_of_edges())
|
self.assertEqual(0, g.number_of_edges())
|
||||||
self.assertEqual(set([a, b, c, d]),
|
self.assertEqual(set([a, b, c, d]),
|
||||||
set(g_utils.get_no_successors(g)))
|
set(g.no_successors_iter()))
|
||||||
self.assertEqual(set([a, b, c, d]),
|
self.assertEqual(set([a, b, c, d]),
|
||||||
set(g_utils.get_no_predecessors(g)))
|
set(g.no_predecessors_iter()))
|
||||||
|
|
||||||
def test_linear_nested_flatten(self):
|
def test_linear_nested_flatten(self):
|
||||||
a, b, c, d = _make_many(4)
|
a, b, c, d = _make_many(4)
|
||||||
@@ -206,8 +203,8 @@ class FlattenTest(test.TestCase):
|
|||||||
(b, c, {'manual': True}),
|
(b, c, {'manual': True}),
|
||||||
(c, d, {'manual': True}),
|
(c, d, {'manual': True}),
|
||||||
])
|
])
|
||||||
self.assertItemsEqual([a], g_utils.get_no_predecessors(g))
|
self.assertItemsEqual([a], g.no_predecessors_iter())
|
||||||
self.assertItemsEqual([d], g_utils.get_no_successors(g))
|
self.assertItemsEqual([d], g.no_successors_iter())
|
||||||
|
|
||||||
def test_graph_flatten_dependencies(self):
|
def test_graph_flatten_dependencies(self):
|
||||||
a = t_utils.ProvidesRequiresTask('a', provides=['x'], requires=[])
|
a = t_utils.ProvidesRequiresTask('a', provides=['x'], requires=[])
|
||||||
@@ -219,8 +216,8 @@ class FlattenTest(test.TestCase):
|
|||||||
self.assertItemsEqual(g.edges(data=True), [
|
self.assertItemsEqual(g.edges(data=True), [
|
||||||
(a, b, {'reasons': set(['x'])})
|
(a, b, {'reasons': set(['x'])})
|
||||||
])
|
])
|
||||||
self.assertItemsEqual([a], g_utils.get_no_predecessors(g))
|
self.assertItemsEqual([a], g.no_predecessors_iter())
|
||||||
self.assertItemsEqual([b], g_utils.get_no_successors(g))
|
self.assertItemsEqual([b], g.no_successors_iter())
|
||||||
|
|
||||||
def test_graph_flatten_nested_requires(self):
|
def test_graph_flatten_nested_requires(self):
|
||||||
a = t_utils.ProvidesRequiresTask('a', provides=['x'], requires=[])
|
a = t_utils.ProvidesRequiresTask('a', provides=['x'], requires=[])
|
||||||
@@ -237,8 +234,8 @@ class FlattenTest(test.TestCase):
|
|||||||
(a, c, {'reasons': set(['x'])}),
|
(a, c, {'reasons': set(['x'])}),
|
||||||
(b, c, {'invariant': True})
|
(b, c, {'invariant': True})
|
||||||
])
|
])
|
||||||
self.assertItemsEqual([a, b], g_utils.get_no_predecessors(g))
|
self.assertItemsEqual([a, b], g.no_predecessors_iter())
|
||||||
self.assertItemsEqual([c], g_utils.get_no_successors(g))
|
self.assertItemsEqual([c], g.no_successors_iter())
|
||||||
|
|
||||||
def test_graph_flatten_nested_provides(self):
|
def test_graph_flatten_nested_provides(self):
|
||||||
a = t_utils.ProvidesRequiresTask('a', provides=[], requires=['x'])
|
a = t_utils.ProvidesRequiresTask('a', provides=[], requires=['x'])
|
||||||
@@ -255,8 +252,8 @@ class FlattenTest(test.TestCase):
|
|||||||
(b, c, {'invariant': True}),
|
(b, c, {'invariant': True}),
|
||||||
(b, a, {'reasons': set(['x'])})
|
(b, a, {'reasons': set(['x'])})
|
||||||
])
|
])
|
||||||
self.assertItemsEqual([b], g_utils.get_no_predecessors(g))
|
self.assertItemsEqual([b], g.no_predecessors_iter())
|
||||||
self.assertItemsEqual([a, c], g_utils.get_no_successors(g))
|
self.assertItemsEqual([a, c], g.no_successors_iter())
|
||||||
|
|
||||||
def test_flatten_checks_for_dups(self):
|
def test_flatten_checks_for_dups(self):
|
||||||
flo = gf.Flow("test").add(
|
flo = gf.Flow("test").add(
|
||||||
@@ -304,8 +301,8 @@ class FlattenTest(test.TestCase):
|
|||||||
(c1, c2, {'retry': True})
|
(c1, c2, {'retry': True})
|
||||||
])
|
])
|
||||||
self.assertIs(c1, g.node[c2]['retry'])
|
self.assertIs(c1, g.node[c2]['retry'])
|
||||||
self.assertItemsEqual([c1], g_utils.get_no_predecessors(g))
|
self.assertItemsEqual([c1], g.no_predecessors_iter())
|
||||||
self.assertItemsEqual([c2], g_utils.get_no_successors(g))
|
self.assertItemsEqual([c2], g.no_successors_iter())
|
||||||
|
|
||||||
def test_flatten_retry_in_linear_flow_with_tasks(self):
|
def test_flatten_retry_in_linear_flow_with_tasks(self):
|
||||||
c = retry.AlwaysRevert("c")
|
c = retry.AlwaysRevert("c")
|
||||||
@@ -318,8 +315,8 @@ class FlattenTest(test.TestCase):
|
|||||||
(c, a, {'retry': True})
|
(c, a, {'retry': True})
|
||||||
])
|
])
|
||||||
|
|
||||||
self.assertItemsEqual([c], g_utils.get_no_predecessors(g))
|
self.assertItemsEqual([c], g.no_predecessors_iter())
|
||||||
self.assertItemsEqual([b], g_utils.get_no_successors(g))
|
self.assertItemsEqual([b], g.no_successors_iter())
|
||||||
self.assertIs(c, g.node[a]['retry'])
|
self.assertIs(c, g.node[a]['retry'])
|
||||||
self.assertIs(c, g.node[b]['retry'])
|
self.assertIs(c, g.node[b]['retry'])
|
||||||
|
|
||||||
@@ -334,8 +331,8 @@ class FlattenTest(test.TestCase):
|
|||||||
(c, b, {'retry': True})
|
(c, b, {'retry': True})
|
||||||
])
|
])
|
||||||
|
|
||||||
self.assertItemsEqual([c], g_utils.get_no_predecessors(g))
|
self.assertItemsEqual([c], g.no_predecessors_iter())
|
||||||
self.assertItemsEqual([a, b], g_utils.get_no_successors(g))
|
self.assertItemsEqual([a, b], g.no_successors_iter())
|
||||||
self.assertIs(c, g.node[a]['retry'])
|
self.assertIs(c, g.node[a]['retry'])
|
||||||
self.assertIs(c, g.node[b]['retry'])
|
self.assertIs(c, g.node[b]['retry'])
|
||||||
|
|
||||||
@@ -352,8 +349,8 @@ class FlattenTest(test.TestCase):
|
|||||||
(b, c, {'manual': True})
|
(b, c, {'manual': True})
|
||||||
])
|
])
|
||||||
|
|
||||||
self.assertItemsEqual([r], g_utils.get_no_predecessors(g))
|
self.assertItemsEqual([r], g.no_predecessors_iter())
|
||||||
self.assertItemsEqual([a, c], g_utils.get_no_successors(g))
|
self.assertItemsEqual([a, c], g.no_successors_iter())
|
||||||
self.assertIs(r, g.node[a]['retry'])
|
self.assertIs(r, g.node[a]['retry'])
|
||||||
self.assertIs(r, g.node[b]['retry'])
|
self.assertIs(r, g.node[b]['retry'])
|
||||||
self.assertIs(r, g.node[c]['retry'])
|
self.assertIs(r, g.node[c]['retry'])
|
||||||
|
|||||||
0
taskflow/types/__init__.py
Normal file
0
taskflow/types/__init__.py
Normal file
122
taskflow/types/graph.py
Normal file
122
taskflow/types/graph.py
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright (C) 2012 Yahoo! Inc. All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import networkx as nx
|
||||||
|
import six
|
||||||
|
|
||||||
|
|
||||||
|
class DiGraph(nx.DiGraph):
|
||||||
|
"""A directed graph subclass with useful utility functions."""
|
||||||
|
def __init__(self, data=None, name=''):
|
||||||
|
super(DiGraph, self).__init__(name=name, data=data)
|
||||||
|
self.frozen = False
|
||||||
|
|
||||||
|
def freeze(self):
|
||||||
|
"""Freezes the graph so that no more mutations can occur."""
|
||||||
|
if not self.frozen:
|
||||||
|
nx.freeze(self)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def get_edge_data(self, u, v, default=None):
|
||||||
|
"""Returns a *copy* of the attribute dictionary associated with edges
|
||||||
|
between (u, v).
|
||||||
|
|
||||||
|
NOTE(harlowja): this differs from the networkx get_edge_data() as that
|
||||||
|
function does not return a copy (but returns a reference to the actual
|
||||||
|
edge data).
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return dict(self.adj[u][v])
|
||||||
|
except KeyError:
|
||||||
|
return default
|
||||||
|
|
||||||
|
def topological_sort(self):
|
||||||
|
"""Return a list of nodes in this graph in topological sort order."""
|
||||||
|
return nx.topological_sort(self)
|
||||||
|
|
||||||
|
def pformat(self):
|
||||||
|
"""Pretty formats your graph into a string representation that includes
|
||||||
|
details about your graph, including; name, type, frozeness, node count,
|
||||||
|
nodes, edge count, edges, graph density and graph cycles (if any).
|
||||||
|
"""
|
||||||
|
lines = []
|
||||||
|
lines.append("Name: %s" % self.name)
|
||||||
|
lines.append("Type: %s" % type(self).__name__)
|
||||||
|
lines.append("Frozen: %s" % nx.is_frozen(self))
|
||||||
|
lines.append("Nodes: %s" % self.number_of_nodes())
|
||||||
|
for n in self.nodes_iter():
|
||||||
|
lines.append(" - %s" % n)
|
||||||
|
lines.append("Edges: %s" % self.number_of_edges())
|
||||||
|
for (u, v, e_data) in self.edges_iter(data=True):
|
||||||
|
if e_data:
|
||||||
|
lines.append(" %s -> %s (%s)" % (u, v, e_data))
|
||||||
|
else:
|
||||||
|
lines.append(" %s -> %s" % (u, v))
|
||||||
|
lines.append("Density: %0.3f" % nx.density(self))
|
||||||
|
cycles = list(nx.cycles.recursive_simple_cycles(self))
|
||||||
|
lines.append("Cycles: %s" % len(cycles))
|
||||||
|
for cycle in cycles:
|
||||||
|
buf = six.StringIO()
|
||||||
|
buf.write("%s" % (cycle[0]))
|
||||||
|
for i in range(1, len(cycle)):
|
||||||
|
buf.write(" --> %s" % (cycle[i]))
|
||||||
|
buf.write(" --> %s" % (cycle[0]))
|
||||||
|
lines.append(" %s" % buf.getvalue())
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
def export_to_dot(self):
|
||||||
|
"""Exports the graph to a dot format (requires pydot library)."""
|
||||||
|
return nx.to_pydot(self).to_string()
|
||||||
|
|
||||||
|
def is_directed_acyclic(self):
|
||||||
|
"""Returns if this graph is a DAG or not."""
|
||||||
|
return nx.is_directed_acyclic_graph(self)
|
||||||
|
|
||||||
|
def no_successors_iter(self):
|
||||||
|
"""Returns an iterator for all nodes with no successors."""
|
||||||
|
for n in self.nodes_iter():
|
||||||
|
if not len(self.successors(n)):
|
||||||
|
yield n
|
||||||
|
|
||||||
|
def no_predecessors_iter(self):
|
||||||
|
"""Returns an iterator for all nodes with no predecessors."""
|
||||||
|
for n in self.nodes_iter():
|
||||||
|
if not len(self.predecessors(n)):
|
||||||
|
yield n
|
||||||
|
|
||||||
|
|
||||||
|
def merge_graphs(graphs, allow_overlaps=False):
|
||||||
|
"""Merges a bunch of graphs into a single graph."""
|
||||||
|
if not graphs:
|
||||||
|
return None
|
||||||
|
graph = graphs[0]
|
||||||
|
for g in graphs[1:]:
|
||||||
|
# This should ensure that the nodes to be merged do not already exist
|
||||||
|
# in the graph that is to be merged into. This could be problematic if
|
||||||
|
# there are duplicates.
|
||||||
|
if not allow_overlaps:
|
||||||
|
# Attempt to induce a subgraph using the to be merged graphs nodes
|
||||||
|
# and see if any graph results.
|
||||||
|
overlaps = graph.subgraph(g.nodes_iter())
|
||||||
|
if len(overlaps):
|
||||||
|
raise ValueError("Can not merge graph %s into %s since there "
|
||||||
|
"are %s overlapping nodes" (g, graph,
|
||||||
|
len(overlaps)))
|
||||||
|
# Keep the target graphs name.
|
||||||
|
name = graph.name
|
||||||
|
graph = nx.algorithms.compose(graph, g)
|
||||||
|
graph.name = name
|
||||||
|
return graph
|
||||||
@@ -16,13 +16,11 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import networkx as nx
|
|
||||||
|
|
||||||
from taskflow import exceptions
|
from taskflow import exceptions
|
||||||
from taskflow import flow
|
from taskflow import flow
|
||||||
from taskflow import retry
|
from taskflow import retry
|
||||||
from taskflow import task
|
from taskflow import task
|
||||||
from taskflow.utils import graph_utils as gu
|
from taskflow.types import graph as gr
|
||||||
from taskflow.utils import misc
|
from taskflow.utils import misc
|
||||||
|
|
||||||
|
|
||||||
@@ -80,7 +78,7 @@ class Flattener(object):
|
|||||||
graph.add_node(retry)
|
graph.add_node(retry)
|
||||||
|
|
||||||
# All graph nodes that have no predecessors should depend on its retry
|
# All graph nodes that have no predecessors should depend on its retry
|
||||||
nodes_to = [n for n in gu.get_no_predecessors(graph) if n != retry]
|
nodes_to = [n for n in graph.no_predecessors_iter() if n != retry]
|
||||||
self._add_new_edges(graph, [retry], nodes_to, RETRY_EDGE_DATA)
|
self._add_new_edges(graph, [retry], nodes_to, RETRY_EDGE_DATA)
|
||||||
|
|
||||||
# Add link to retry for each node of subgraph that hasn't
|
# Add link to retry for each node of subgraph that hasn't
|
||||||
@@ -91,34 +89,37 @@ class Flattener(object):
|
|||||||
|
|
||||||
def _flatten_task(self, task):
|
def _flatten_task(self, task):
|
||||||
"""Flattens a individual task."""
|
"""Flattens a individual task."""
|
||||||
graph = nx.DiGraph(name=task.name)
|
graph = gr.DiGraph(name=task.name)
|
||||||
graph.add_node(task)
|
graph.add_node(task)
|
||||||
return graph
|
return graph
|
||||||
|
|
||||||
def _flatten_flow(self, flow):
|
def _flatten_flow(self, flow):
|
||||||
"""Flattens a graph flow."""
|
"""Flattens a graph flow."""
|
||||||
graph = nx.DiGraph(name=flow.name)
|
graph = gr.DiGraph(name=flow.name)
|
||||||
|
|
||||||
# Flatten all nodes into a single subgraph per node.
|
# Flatten all nodes into a single subgraph per node.
|
||||||
subgraph_map = {}
|
subgraph_map = {}
|
||||||
for item in flow:
|
for item in flow:
|
||||||
subgraph = self._flatten(item)
|
subgraph = self._flatten(item)
|
||||||
subgraph_map[item] = subgraph
|
subgraph_map[item] = subgraph
|
||||||
graph = gu.merge_graphs([graph, subgraph])
|
graph = gr.merge_graphs([graph, subgraph])
|
||||||
|
|
||||||
# Reconnect all node edges to their corresponding subgraphs.
|
# Reconnect all node edges to their corresponding subgraphs.
|
||||||
for (u, v, attrs) in flow.iter_links():
|
for (u, v, attrs) in flow.iter_links():
|
||||||
|
u_g = subgraph_map[u]
|
||||||
|
v_g = subgraph_map[v]
|
||||||
if any(attrs.get(k) for k in ('invariant', 'manual', 'retry')):
|
if any(attrs.get(k) for k in ('invariant', 'manual', 'retry')):
|
||||||
# Connect nodes with no predecessors in v to nodes with
|
# Connect nodes with no predecessors in v to nodes with
|
||||||
# no successors in u (thus maintaining the edge dependency).
|
# no successors in u (thus maintaining the edge dependency).
|
||||||
self._add_new_edges(graph,
|
self._add_new_edges(graph,
|
||||||
gu.get_no_successors(subgraph_map[u]),
|
u_g.no_successors_iter(),
|
||||||
gu.get_no_predecessors(subgraph_map[v]),
|
v_g.no_predecessors_iter(),
|
||||||
edge_attrs=attrs)
|
edge_attrs=attrs)
|
||||||
else:
|
else:
|
||||||
# This is dependency-only edge, connect corresponding
|
# This is dependency-only edge, connect corresponding
|
||||||
# providers and consumers.
|
# providers and consumers.
|
||||||
for provider in subgraph_map[u]:
|
for provider in u_g:
|
||||||
for consumer in subgraph_map[v]:
|
for consumer in v_g:
|
||||||
reasons = provider.provides & consumer.requires
|
reasons = provider.provides & consumer.requires
|
||||||
if reasons:
|
if reasons:
|
||||||
graph.add_edge(provider, consumer, reasons=reasons)
|
graph.add_edge(provider, consumer, reasons=reasons)
|
||||||
@@ -143,7 +144,7 @@ class Flattener(object):
|
|||||||
# and not under all cases.
|
# and not under all cases.
|
||||||
if LOG.isEnabledFor(logging.DEBUG):
|
if LOG.isEnabledFor(logging.DEBUG):
|
||||||
LOG.debug("Translated '%s' into a graph:", item)
|
LOG.debug("Translated '%s' into a graph:", item)
|
||||||
for line in gu.pformat(graph).splitlines():
|
for line in graph.pformat().splitlines():
|
||||||
# Indent it so that it's slightly offset from the above line.
|
# Indent it so that it's slightly offset from the above line.
|
||||||
LOG.debug(" %s", line)
|
LOG.debug(" %s", line)
|
||||||
|
|
||||||
@@ -168,10 +169,9 @@ class Flattener(object):
|
|||||||
self._pre_flatten()
|
self._pre_flatten()
|
||||||
graph = self._flatten(self._root)
|
graph = self._flatten(self._root)
|
||||||
self._post_flatten(graph)
|
self._post_flatten(graph)
|
||||||
|
self._graph = graph
|
||||||
if self._freeze:
|
if self._freeze:
|
||||||
self._graph = nx.freeze(graph)
|
self._graph.freeze()
|
||||||
else:
|
|
||||||
self._graph = graph
|
|
||||||
return self._graph
|
return self._graph
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,98 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# Copyright (C) 2012 Yahoo! Inc. All Rights Reserved.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
import networkx as nx
|
|
||||||
import six
|
|
||||||
|
|
||||||
|
|
||||||
def get_edge_attrs(graph, u, v):
|
|
||||||
"""Gets the dictionary of edge attributes between u->v (or none)."""
|
|
||||||
if not graph.has_edge(u, v):
|
|
||||||
return None
|
|
||||||
return dict(graph.adj[u][v])
|
|
||||||
|
|
||||||
|
|
||||||
def merge_graphs(graphs, allow_overlaps=False):
|
|
||||||
if not graphs:
|
|
||||||
return None
|
|
||||||
graph = graphs[0]
|
|
||||||
for g in graphs[1:]:
|
|
||||||
# This should ensure that the nodes to be merged do not already exist
|
|
||||||
# in the graph that is to be merged into. This could be problematic if
|
|
||||||
# there are duplicates.
|
|
||||||
if not allow_overlaps:
|
|
||||||
# Attempt to induce a subgraph using the to be merged graphs nodes
|
|
||||||
# and see if any graph results.
|
|
||||||
overlaps = graph.subgraph(g.nodes_iter())
|
|
||||||
if len(overlaps):
|
|
||||||
raise ValueError("Can not merge graph %s into %s since there "
|
|
||||||
"are %s overlapping nodes" (g, graph,
|
|
||||||
len(overlaps)))
|
|
||||||
# Keep the target graphs name.
|
|
||||||
name = graph.name
|
|
||||||
graph = nx.algorithms.compose(graph, g)
|
|
||||||
graph.name = name
|
|
||||||
return graph
|
|
||||||
|
|
||||||
|
|
||||||
def get_no_successors(graph):
|
|
||||||
"""Returns an iterator for all nodes with no successors."""
|
|
||||||
for n in graph.nodes_iter():
|
|
||||||
if not len(graph.successors(n)):
|
|
||||||
yield n
|
|
||||||
|
|
||||||
|
|
||||||
def get_no_predecessors(graph):
|
|
||||||
"""Returns an iterator for all nodes with no predecessors."""
|
|
||||||
for n in graph.nodes_iter():
|
|
||||||
if not len(graph.predecessors(n)):
|
|
||||||
yield n
|
|
||||||
|
|
||||||
|
|
||||||
def pformat(graph):
|
|
||||||
"""Pretty formats your graph into a string representation that includes
|
|
||||||
details about your graph, including; name, type, frozeness, node count,
|
|
||||||
nodes, edge count, edges, graph density and graph cycles (if any).
|
|
||||||
"""
|
|
||||||
lines = []
|
|
||||||
lines.append("Name: %s" % graph.name)
|
|
||||||
lines.append("Type: %s" % type(graph).__name__)
|
|
||||||
lines.append("Frozen: %s" % nx.is_frozen(graph))
|
|
||||||
lines.append("Nodes: %s" % graph.number_of_nodes())
|
|
||||||
for n in graph.nodes_iter():
|
|
||||||
lines.append(" - %s" % n)
|
|
||||||
lines.append("Edges: %s" % graph.number_of_edges())
|
|
||||||
for (u, v, e_data) in graph.edges_iter(data=True):
|
|
||||||
if e_data:
|
|
||||||
lines.append(" %s -> %s (%s)" % (u, v, e_data))
|
|
||||||
else:
|
|
||||||
lines.append(" %s -> %s" % (u, v))
|
|
||||||
lines.append("Density: %0.3f" % nx.density(graph))
|
|
||||||
cycles = list(nx.cycles.recursive_simple_cycles(graph))
|
|
||||||
lines.append("Cycles: %s" % len(cycles))
|
|
||||||
for cycle in cycles:
|
|
||||||
buf = six.StringIO()
|
|
||||||
buf.write(str(cycle[0]))
|
|
||||||
for i in range(1, len(cycle)):
|
|
||||||
buf.write(" --> %s" % (cycle[i]))
|
|
||||||
buf.write(" --> %s" % (cycle[0]))
|
|
||||||
lines.append(" %s" % buf.getvalue())
|
|
||||||
return "\n".join(lines)
|
|
||||||
|
|
||||||
|
|
||||||
def export_graph_to_dot(graph):
|
|
||||||
"""Exports the graph to a dot format (requires pydot library)."""
|
|
||||||
return nx.to_pydot(graph).to_string()
|
|
||||||
@@ -11,10 +11,8 @@ import optparse
|
|||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
import networkx as nx
|
|
||||||
|
|
||||||
from taskflow import states
|
from taskflow import states
|
||||||
from taskflow.utils import graph_utils as gu
|
from taskflow.types import graph as gr
|
||||||
|
|
||||||
|
|
||||||
def mini_exec(cmd, ok_codes=(0,)):
|
def mini_exec(cmd, ok_codes=(0,)):
|
||||||
@@ -31,7 +29,7 @@ def mini_exec(cmd, ok_codes=(0,)):
|
|||||||
|
|
||||||
def make_svg(graph, output_filename, output_format):
|
def make_svg(graph, output_filename, output_format):
|
||||||
# NOTE(harlowja): requires pydot!
|
# NOTE(harlowja): requires pydot!
|
||||||
gdot = gu.export_graph_to_dot(graph)
|
gdot = graph.export_to_dot()
|
||||||
if output_format == 'dot':
|
if output_format == 'dot':
|
||||||
output = gdot
|
output = gdot
|
||||||
elif output_format in ('svg', 'svgz', 'png'):
|
elif output_format in ('svg', 'svgz', 'png'):
|
||||||
@@ -62,7 +60,7 @@ def main():
|
|||||||
if options.filename is None:
|
if options.filename is None:
|
||||||
options.filename = 'states.%s' % options.format
|
options.filename = 'states.%s' % options.format
|
||||||
|
|
||||||
g = nx.DiGraph(name="State transitions")
|
g = gr.DiGraph(name="State transitions")
|
||||||
if not options.tasks:
|
if not options.tasks:
|
||||||
source = states._ALLOWED_FLOW_TRANSITIONS
|
source = states._ALLOWED_FLOW_TRANSITIONS
|
||||||
else:
|
else:
|
||||||
|
|||||||
Reference in New Issue
Block a user