|
@@ -6,7 +6,7 @@
|
|
|
Custom types and data structures.
|
|
|
|
|
|
"""
|
|
|
-from __future__ import absolute_import, print_function
|
|
|
+from __future__ import absolute_import, print_function, unicode_literals
|
|
|
|
|
|
import sys
|
|
|
import time
|
|
@@ -20,6 +20,106 @@ from kombu.utils.limits import TokenBucket # noqa
|
|
|
|
|
|
from .utils.functional import LRUCache, first, uniq # noqa
|
|
|
|
|
|
+DOT_HEAD = """
|
|
|
+{IN}{type} {id} {{
|
|
|
+{INp}graph [{attrs}]
|
|
|
+"""
|
|
|
+DOT_ATTR = '{name}={value}'
|
|
|
+DOT_NODE = '{INp}"{0}" [{attrs}]'
|
|
|
+DOT_EDGE = '{INp}"{0}" {dir} "{1}" [{attrs}]'
|
|
|
+DOT_ATTRSEP = ', '
|
|
|
+DOT_DIRS = {'graph': '--', 'digraph': '->'}
|
|
|
+DOT_TAIL = '{IN}}}'
|
|
|
+
|
|
|
+
|
|
|
+class GraphFormatter(object):
|
|
|
+ _attr = DOT_ATTR.strip()
|
|
|
+ _node = DOT_NODE.strip()
|
|
|
+ _edge = DOT_EDGE.strip()
|
|
|
+ _head = DOT_HEAD.strip()
|
|
|
+ _tail = DOT_TAIL.strip()
|
|
|
+ _attrsep = DOT_ATTRSEP
|
|
|
+ _dirs = dict(DOT_DIRS)
|
|
|
+
|
|
|
+ scheme = {
|
|
|
+ 'shape': 'box',
|
|
|
+ 'arrowhead': 'vee',
|
|
|
+ 'style': 'filled',
|
|
|
+ 'fontname': 'Helvetica Neue',
|
|
|
+ }
|
|
|
+ node_scheme = {
|
|
|
+ 'fillcolor': 'palegreen3',
|
|
|
+ 'color': 'palegreen4',
|
|
|
+ }
|
|
|
+ term_scheme = {
|
|
|
+ 'fillcolor': 'palegreen1',
|
|
|
+ 'color': 'palegreen2',
|
|
|
+ }
|
|
|
+ edge_scheme = {
|
|
|
+ 'color': 'darkseagreen4',
|
|
|
+ 'arrowcolor': 'black',
|
|
|
+ 'arrowsize': 0.7,
|
|
|
+ }
|
|
|
+ graph_scheme = {'bgcolor': 'mintcream'}
|
|
|
+
|
|
|
+ def __init__(self, root=None, type=None, id=None, indent=0, inw=' ' * 4):
|
|
|
+ self.id = id or 'dependencies'
|
|
|
+ self.root = root
|
|
|
+ self.type = type or 'digraph'
|
|
|
+ self.direction = self._dirs[self.type]
|
|
|
+ self.IN = inw * (indent or 0)
|
|
|
+ self.INp = self.IN + inw
|
|
|
+ #self.graph_scheme = dict(self.graph_scheme, root=self.label(self.root))
|
|
|
+
|
|
|
+ def attr(self, name, value):
|
|
|
+ value = '"{0}"'.format(str(value))
|
|
|
+ return self.FMT(self._attr, name=name, value=value)
|
|
|
+
|
|
|
+ def attrs(self, d, scheme=None):
|
|
|
+ d = dict(self.scheme, **dict(scheme, **d or {}) if scheme else d)
|
|
|
+ return self._attrsep.join(self.attr(k, v) for k, v in d.iteritems())
|
|
|
+
|
|
|
+ def head(self, **attrs):
|
|
|
+ return self.FMT(self._head, id=self.id, type=self.type,
|
|
|
+ attrs=self.attrs(attrs, self.graph_scheme),
|
|
|
+ )
|
|
|
+
|
|
|
+ def tail(self):
|
|
|
+ return self.FMT(self._tail)
|
|
|
+
|
|
|
+ def label(self, obj):
|
|
|
+ return obj
|
|
|
+
|
|
|
+ def node(self, obj, **attrs):
|
|
|
+ return self.draw_node(obj, self.node_scheme, attrs)
|
|
|
+
|
|
|
+ def terminal_node(self, obj, **attrs):
|
|
|
+ return self.draw_node(obj, self.term_scheme, attrs)
|
|
|
+
|
|
|
+ def edge(self, a, b, **attrs):
|
|
|
+ return self.draw_edge(a, b, **attrs)
|
|
|
+
|
|
|
+ def _enc(self, s):
|
|
|
+ return s.encode('utf-8', 'ignore')
|
|
|
+
|
|
|
+ def FMT(self, fmt, *args, **kwargs):
|
|
|
+ return self._enc(fmt.format(
|
|
|
+ *args, **dict(kwargs, IN=self.IN, INp=self.INp)
|
|
|
+ ))
|
|
|
+
|
|
|
+ def draw_edge(self, a, b, scheme=None, attrs=None):
|
|
|
+ return self.FMT(self._edge, self.label(a), self.label(b),
|
|
|
+ dir=self.direction, attrs=self.attrs(attrs, self.edge_scheme),
|
|
|
+ )
|
|
|
+
|
|
|
+ def draw_node(self, obj, scheme=None, attrs=None):
|
|
|
+ return self.FMT(self._node, self.label(obj),
|
|
|
+ attrs=self.attrs(attrs, scheme),
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
|
|
|
class CycleError(Exception):
|
|
|
"""A cycle was detected in an acyclic graph."""
|
|
@@ -40,7 +140,8 @@ class DependencyGraph(object):
|
|
|
|
|
|
"""
|
|
|
|
|
|
- def __init__(self, it=None):
|
|
|
+ def __init__(self, it=None, formatter=None):
|
|
|
+ self.formatter = formatter or GraphFormatter()
|
|
|
self.adjacent = {}
|
|
|
if it is not None:
|
|
|
self.update(it)
|
|
@@ -158,20 +259,33 @@ class DependencyGraph(object):
|
|
|
|
|
|
return result
|
|
|
|
|
|
- def to_dot(self, fh, ws=' ' * 4):
|
|
|
+ def to_dot(self, fh, formatter=None):
|
|
|
"""Convert the graph to DOT format.
|
|
|
|
|
|
:param fh: A file, or a file-like object to write the graph to.
|
|
|
|
|
|
"""
|
|
|
+ seen = set()
|
|
|
+ draw = formatter or self.formatter
|
|
|
P = partial(print, file=fh)
|
|
|
- P('digraph dependencies {')
|
|
|
+
|
|
|
+ def if_not_seen(fun, obj):
|
|
|
+ if draw.label(obj) not in seen:
|
|
|
+ P(fun(obj))
|
|
|
+ seen.add(draw.label(obj))
|
|
|
+
|
|
|
+ P(draw.head())
|
|
|
for obj, adjacent in self.iteritems():
|
|
|
+ objl = draw.label(obj)
|
|
|
if not adjacent:
|
|
|
- P(ws + '"{0}"'.format(obj))
|
|
|
+ if_not_seen(draw.terminal_node, obj)
|
|
|
for req in adjacent:
|
|
|
- P(ws + '"{0}" -> "{1}"'.format(obj, req))
|
|
|
- P('}')
|
|
|
+ if_not_seen(draw.node, obj)
|
|
|
+ P(draw.edge(obj, req))
|
|
|
+ P(draw.tail())
|
|
|
+
|
|
|
+ def format(self, obj):
|
|
|
+ return self.formatter(obj) if self.formatter else obj
|
|
|
|
|
|
def __iter__(self):
|
|
|
return iter(self.adjacent)
|