# vim: set fileencoding=UTF-8: # Copyright 2013 Red Hat, Inc. # Author: Jan Pokorný # Distributed under GPLv2+; generated content under CC-BY-SA 3.0 # (to view a copy, visit http://creativecommons.org/licenses/by-sa/3.0/) """Custom wrapping of pydot""" from os.path import extsep from sys import stderr from pydot import Dot, Edge, Node, Subgraph REGISTRY = {} # modify/mutate BLACKLIST in order to filter some elements out # (not a beautiful, rather ad-hoc design, I know) BLACKLIST = [] # # Customization via meta level # # obfuscation contest in recursively yielding base classes up to given set bases = lambda x: (lambda f_, *xs_: f_(f_, *xs_)) \ (lambda f, x=[], *xs: [x] + f(f, *x.__bases__) + f(f, *xs) if x not in ([], LibSubgraph, LibNode, LibEdge) else [], x) bl_test = lambda ix: not set(bases(ix.__class__)).intersection(BLACKLIST) def bl_map_nodes_edges(nodes, edges, prev_nnames=None): nnames = prev_nnames if prev_nnames is not None else set() for n in nodes: n.get_attributes()['style'] = 'invis' nnames.add(n.get_name()) for e in edges: if not bl_test(e) \ or e.get_source() in nnames or e.get_destination() in nnames: e.get_attributes()['style'] = 'invis' class LibMeta(type): promote_url = False def __new__(cls, name, bases, attrs): ret = super(LibMeta, cls).__new__(cls, name, bases, attrs) if REGISTRY.setdefault(name, ret) is not ret: raise RuntimeError("Unexpected redefinition in REGISTRY") return ret def __init__(cls, name, bases, attrs): super(LibMeta, cls).__init__(name, bases, attrs) old_init = cls.__init__ def new_init(self, *args, **kwargs): subgraphs = kwargs.pop('_subgraphs', ()) nodes = kwargs.pop('_nodes', ()) edges = kwargs.pop('_edges', ()) if not hasattr(self, 'nnames'): self.nnames = set() nnames = self.nnames map(lambda x: nnames.update(getattr(x, 'nnames')), tuple(subgraphs) + tuple(nodes) + tuple(edges)) for akey, avalue in getattr(cls, 'defaults', {}).iteritems(): kwargs.setdefault(akey, avalue) if type(self).promote_url: if hasattr(self, 'web'): kwargs['URL'] = getattr(self, 'web', None) else: url = "class://" + self.__class__.__name__ for l in ('common_src_of', 'common_dst_of'): url += ';' url += getattr(self, l).__name__ if hasattr(self, l) else '' kwargs['URL'] = url old_init(self, *args, **kwargs) super(cls, self).__init__(*args, **kwargs) bl_map_nodes_edges(filter(lambda x: not bl_test(x), nodes), edges, nnames) for n in nodes: self.add_node(n) for e in edges: self.add_edge(e) for s in filter(bl_test, subgraphs): s and self.add_subgraph(s) cls.__init__ = new_init class LibDot(Dot): __metaclass__ = LibMeta def __init__(self, *args, **kwargs): kwargs.setdefault('graph_type', 'digraph') super(LibDot, self).__init__(*args, **kwargs) #self.set_suppress_disconnected(True) class LibSubgraph(Subgraph): __metaclass__ = LibMeta def __init__(self, *args, **kwargs): super(LibSubgraph, self).__init__(*args, **kwargs) #self. set_suppress_disconnected(True) class LibNode(Node): __metaclass__ = LibMeta class LibEdge(Edge): __metaclass__ = LibMeta # Attributes class LibAttributeMeta(type): def __getattr__(cls, what): return (cls.__name__.lower(), what) class LibAttribute(object): __metaclass__ = LibAttributeMeta def __new__(self, what): return (self.__name__.lower(), what) #return getattr(self, what) # # main-helpers # def gen_graph(graph, blacklist=(), **kwargs): # can eventually restore the state BLACKLIST[:] = blacklist return graph() def xdot_graph(graph, *args, **kwargs): import gtk import gtk.gdk import pango try: import xdot except ImportError: print >>stderr, 'missing xdot; (currently) recommended way to grab it:' print >>stderr, 'pip install --user https://github.com/jrfonseca/xdot.py/archive/master.zip' raise class LibDotWidget(xdot.DotWidget): """Overriden so as to serve our info tooltip purpose""" def __init__(self, *args, **kwargs): super(LibDotWidget, self).__init__(*args, **kwargs) self.connect('leave-notify-event', lambda self, *args: self.set_tooltip_markup(None)) def on_click(self, element, event): """Overload url attribute as it is the only passed internally""" if element is None: return False elif not hasattr(element, 'url'): assert isinstance(element, xdot.Edge) for access, i in ((element.src, 1), (element.dst, 2)): which = access.url.split('class://', 1)[1].split(';')[i] if which in REGISTRY: # XXX: check matching label to be perfectly sure cls = REGISTRY[which] break else: return False else: cls = REGISTRY[element.url.split('class://', 1)[1].split(';')[0]] # create the resulting markup markup = "" # XXX: sort the keys in dicts? if hasattr(cls, 'summary'): markup += "" + cls.summary + "\n\n" if hasattr(cls, 'web'): markup += "" + cls.web + "\n\n" if hasattr(cls, 'repo'): markup += "repository:\n" for name, value in cls.repo.iteritems(): markup += "* " + name + ": " + str(value) + "\n" markup += '\n' if hasattr(cls, 'man'): markup += "man:\n" for item in cls.man: markup += "* " + item + "\n" markup += '\n' if hasattr(cls, 'doc'): markup += "documentation:\n" for name, value in cls.doc.iteritems(): markup += "* " + name + ": " + value + "\n" markup += '\n' if hasattr(cls, 'ids'): markup += "identifiers:\n" for name, value in cls.ids.iteritems(): markup += "* " + name + ": " + value + "\n" markup += '\n' if hasattr(cls, 'secprops'): markup += "security properties:\n" for name, value in cls.secprops.iteritems(): markup += "* " + name + ": " + str(value) + "\n" markup += '\n' if hasattr(cls, 'miscprops'): markup += "miscellaneous properties:\n" for name, value in cls.miscprops.iteritems(): markup += "* " + name + ": " + str(value) + "\n" markup += '\n' markup = markup.rstrip('\n') print pango.parse_markup(markup)[1].join(2 * ['\n' + 3*'=' + '\n']) self.set_tooltip_markup(markup) return False class LibDotWindow(xdot.DotWindow): # heavily inspired from http://zetcode.com/gui/pygtk/menus/ def on_toggle_item(self, widget): label = widget.get_label() bl = self._kwargs.setdefault('blacklist', []) change = False cls = REGISTRY[label] if widget.active and cls in bl: bl.remove(cls) bs = bases(cls) bs.remove(cls) for item in filter(lambda x: isinstance(x, gtk.CheckMenuItem) and REGISTRY[x.get_label()] in bs, widget.get_parent().get_children()): if not item.get_active(): item.set_active(True) change = True elif not widget.active and cls not in bl: bl.append(cls) change = True if change and self._can_change: self.set_xdotcode( xdotcode = gen_graph(self._graph, **self._kwargs).create( format='xdot', prog=self._prog ) ) def on_clean_all(self, widget): self._can_change = False for item in filter(lambda x: isinstance(x, gtk.CheckMenuItem), widget.get_parent().get_children()): item.set_active(False) self.set_xdotcode( xdotcode = gen_graph(self._graph, **self._kwargs).create( format='xdot', prog=self._prog ) ) self._can_change = True def on_set_all(self, widget): self._can_change = False for item in filter(lambda x: isinstance(x, gtk.CheckMenuItem), widget.get_parent().get_children()): item.set_active(True) self.set_xdotcode( xdotcode = gen_graph(self._graph, **self._kwargs).create( format='xdot', prog=self._prog ) ) self._can_change = True def __init__(self, graph, *args, **kwargs): self._graph = graph self._prog = ['dot'] + list(args) self._kwargs = kwargs try: super(LibDotWindow, self).__init__(LibDotWidget()) except TypeError: print >>stderr, "xdot too old, you won't get tooltips :-/" super(LibDotWindow, self).__init__() xdotcode = gen_graph(self._graph, **self._kwargs).create( format='xdot', prog=self._prog ) title = xdotcode[xdotcode.find('digraph', 0, 512) + len('digraph') : xdotcode.find('{', 0, 512)].strip(' \'"') self.base_title = title self.update_title() self.set_xdotcode(xdotcode) self._can_change = True mb = gtk.MenuBar() filemenu = gtk.Menu() filem = gtk.MenuItem("File") filem.set_submenu(filemenu) edgemenu = gtk.Menu() edge = gtk.MenuItem("Edge") edge.set_submenu(edgemenu) edgemenuitem = gtk.MenuItem("Clean all") edgemenuitem.connect("activate", self.on_clean_all) edgemenu.append(edgemenuitem) edgemenuitem = gtk.MenuItem("Set all") edgemenuitem.connect("activate", self.on_set_all) edgemenu.append(edgemenuitem) edgemenu.append(gtk.SeparatorMenuItem()) nodemenu = gtk.Menu() node = gtk.MenuItem("Node") node.set_submenu(nodemenu) nodemenuitem = gtk.MenuItem("Clean all") nodemenuitem.connect("activate", self.on_clean_all) nodemenu.append(nodemenuitem) nodemenuitem = gtk.MenuItem("Set all") nodemenuitem.connect("activate", self.on_set_all) nodemenu.append(nodemenuitem) nodemenu.append(gtk.SeparatorMenuItem()) # memoization + convenience EdgeInvisible = REGISTRY.get('EdgeInvisible') NodeInvisible = REGISTRY.get('NodeInvisible') for i in REGISTRY.itervalues(): try: if issubclass(i, LibEdge): if i in (LibEdge, EdgeInvisible): continue imenuitem = gtk.CheckMenuItem("%s" % i.__name__) imenuitem.set_active(True) imenuitem.connect("activate", self.on_toggle_item) edgemenu.append(imenuitem) elif issubclass(i, LibNode): if i in (LibNode, NodeInvisible): continue imenuitem = gtk.CheckMenuItem("%s" % i.__name__) imenuitem.set_active(True) imenuitem.connect("activate", self.on_toggle_item) nodemenu.append(imenuitem) except: pass exit = gtk.MenuItem("Exit") exit.connect("activate", gtk.main_quit) filemenu.append(exit) mb.append(filem) mb.append(edge) mb.append(node) sb = gtk.Statusbar() sb.push(1, "Ready") vbox = self.get_children()[0] vbox.pack_start(mb, False, False, 0) vbox.pack_start(sb, False, False, 0) vbox.reorder_child(mb, 0) self.connect("destroy", gtk.main_quit) self.maximize() self.show_all() window = LibDotWindow(graph, *args, **kwargs) window.connect('destroy', gtk.main_quit) gtk.main() def main(graph, argv, *args, **kws): # -x deprecated, supported for backward compatibility x, argv = (1, argv[1:]) if len(argv) <= 1 or argv[1] == '-x' else (0, argv) output = kws.pop('output', 'sinenomine') if x: xdot_graph(graph, *args, **kws) else: LibMeta.promote_url = True ext = argv[1] if len(argv) > 1 else 'pdf' fmt = {'dot': 'raw'}.get(ext, ext) output += extsep + ext gen_graph(graph, **kws).write(output, format=fmt, prog=['dot'].extend(args))