Files
pyan/graph.py
2016-12-14 19:02:59 +02:00

177 lines
5.8 KiB
Python

import re
import logging
from colors import Colorizer
UNSAFE_IDS = {"graph"}
class GraphNode(object):
"""
A node.
flavor is meant to be used one day for things like 'source file', 'class',
'function'...
"""
def __init__(
self, id, label='', flavor='',
fill_color='', text_color='', group=''):
self.id = id
self.label = label
self.flavor = ''
self.fill_color = fill_color
self.text_color = text_color
self.group = group
def __repr__(self):
optionals = [
repr(s) for s in [
self.label, self.flavor,
self.fill_color, self.text_color, self.group] if s]
if optionals:
return ('GraphNode(' + repr(self.id) +
', ' + ', '.join(optionals)+')')
else:
return 'GraphNode(' + repr(self.id) + ')'
@property
def safe_id(self):
if self.id in UNSAFE_IDS:
return "%sX" % self.id
return self.id
class GraphEdge(object):
"""
An edge
flavor is meant to be 'uses' or 'defines'
"""
def __init__(self, source, target, flavor):
self.source = source
self.target = target
self.flavor = flavor
def __repr__(self):
return (
'Edge('+self.source.label+' '+self.flavor+' ' +
self.target.label+')')
class Graph(object):
def __init__(
self, id, label, nodes=None, edges=None, subgraphs=None,
grouped=False):
self.id = id
self.label = label
self.nodes = nodes or []
self.edges = edges or []
self.subgraphs = subgraphs or []
self.grouped = grouped
@property
def safe_id(self):
if self.id in UNSAFE_IDS:
return "%sX" % self.id
return self.id
@classmethod
def from_visitor(cls, visitor, options=None, logger=None):
colored = options.get('colored', False)
nested = options.get('nested_groups', False)
# enforce grouped when nested
grouped = nested or options.get('grouped', False)
draw_defines = options.get('draw_defines', False)
draw_uses = options.get('draw_uses', False)
logger = logger or logging.getLogger(__name__)
colorizer = Colorizer(colored, logger)
# collect and sort defined nodes
visited_nodes = []
for name in visitor.nodes:
for node in visitor.nodes[name]:
if node.defined:
visited_nodes.append(node)
visited_nodes.sort()
nodes_dict = dict()
root_graph = cls('G', label='', grouped=grouped)
subgraph = root_graph
namespace_stack = []
prev_namespace = ''
for node in visited_nodes:
logger.info('Looking at %s' % node.name)
# Create the node itself and add it to nodes_dict
fill_RGBA, text_RGB, idx = colorizer.make_colors(node)
graph_node = GraphNode(
node.get_label(),
label=node.get_short_name(),
fill_color=fill_RGBA,
text_color=text_RGB,
group=idx)
nodes_dict[node] = graph_node
# new namespace? (NOTE: nodes sorted by namespace!)
if grouped and node.namespace != prev_namespace:
logger.info(
'New namespace %s, old was %s'
% (node.namespace, prev_namespace))
label = node.namespace.replace('.', '__').replace('*', '')
subgraph = cls(label, node.namespace)
if nested:
# Pop the stack until the newly found namespace is within
# one of the parent namespaces, or until the stack runs out
# (i.e. this is a sibling).
j = len(namespace_stack) - 1
if j >= 0:
m = re.match(namespace_stack[j].label, node.namespace)
# The '.' check catches siblings in cases like
# MeshGenerator vs. Mesh.
while (
m is None or
m.end() == len(node.namespace) or
node.namespace[m.end()] != '.'):
namespace_stack.pop(j)
j -= 1
if j < 0:
break
m = re.match(
namespace_stack[j].label, node.namespace)
parentgraph = namespace_stack[j] if j >= 0 else root_graph
parentgraph.subgraphs.append(subgraph)
namespace_stack.append(subgraph)
else:
root_graph.subgraphs.append(subgraph)
prev_namespace = node.namespace
subgraph.nodes.append(graph_node)
# Now add edges
if draw_defines:
for n in visitor.defines_edges:
for n2 in visitor.defines_edges[n]:
if n2.defined and n2 != n:
root_graph.edges.append(
GraphEdge(
nodes_dict[n],
nodes_dict[n2],
'defines'))
if draw_uses:
for n in visitor.uses_edges:
for n2 in visitor.uses_edges[n]:
if n2.defined and n2 != n:
root_graph.edges.append(
GraphEdge(
nodes_dict[n],
nodes_dict[n2],
'uses'))
return root_graph