# Copyright (c) 2008-2010, 2013-2014 LOGILAB S.A. (Paris, FRANCE) # Copyright (c) 2014 Arun Persaud # Copyright (c) 2015-2018, 2020 Claudiu Popa # Copyright (c) 2015 Mike Frysinger # Copyright (c) 2015 Florian Bruhin # Copyright (c) 2015 Ionel Cristian Maries # Copyright (c) 2018, 2020 Anthony Sottile # Copyright (c) 2018 ssolanki # Copyright (c) 2019 Pierre Sassoulas # Copyright (c) 2019 Kylian # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html # For details: https://github.com/PyCQA/pylint/blob/master/COPYING """Utilities for creating VCG and Dot diagrams""" from pylint.graph import DotBackend from pylint.pyreverse.utils import is_exception from pylint.pyreverse.vcgutils import VCGPrinter class DiagramWriter: """base class for writing project diagrams """ def __init__(self, config, styles): self.config = config self.pkg_edges, self.inh_edges, self.imp_edges, self.association_edges = styles self.printer = None # defined in set_printer def write(self, diadefs): """write files for according to """ for diagram in diadefs: basename = diagram.title.strip().replace(" ", "_") file_name = "%s.%s" % (basename, self.config.output_format) self.set_printer(file_name, basename) if diagram.TYPE == "class": self.write_classes(diagram) else: self.write_packages(diagram) self.close_graph() def write_packages(self, diagram): """write a package diagram""" # sorted to get predictable (hence testable) results for i, obj in enumerate(sorted(diagram.modules(), key=lambda x: x.title)): self.printer.emit_node(i, label=self.get_title(obj), shape="box") obj.fig_id = i # package dependencies for rel in diagram.get_relationships("depends"): self.printer.emit_edge( rel.from_object.fig_id, rel.to_object.fig_id, **self.pkg_edges ) def write_classes(self, diagram): """write a class diagram""" # sorted to get predictable (hence testable) results for i, obj in enumerate(sorted(diagram.objects, key=lambda x: x.title)): self.printer.emit_node(i, **self.get_values(obj)) obj.fig_id = i # inheritance links for rel in diagram.get_relationships("specialization"): self.printer.emit_edge( rel.from_object.fig_id, rel.to_object.fig_id, **self.inh_edges ) # implementation links for rel in diagram.get_relationships("implements"): self.printer.emit_edge( rel.from_object.fig_id, rel.to_object.fig_id, **self.imp_edges ) # generate associations for rel in diagram.get_relationships("association"): self.printer.emit_edge( rel.from_object.fig_id, rel.to_object.fig_id, label=rel.name, **self.association_edges ) def set_printer(self, file_name, basename): """set printer""" raise NotImplementedError def get_title(self, obj): """get project title""" raise NotImplementedError def get_values(self, obj): """get label and shape for classes.""" raise NotImplementedError def close_graph(self): """finalize the graph""" raise NotImplementedError class DotWriter(DiagramWriter): """write dot graphs from a diagram definition and a project """ def __init__(self, config): styles = [ dict(arrowtail="none", arrowhead="open"), dict(arrowtail="none", arrowhead="empty"), dict(arrowtail="node", arrowhead="empty", style="dashed"), dict( fontcolor="green", arrowtail="none", arrowhead="diamond", style="solid" ), ] DiagramWriter.__init__(self, config, styles) def set_printer(self, file_name, basename): """initialize DotWriter and add options for layout. """ layout = dict(rankdir="BT") self.printer = DotBackend(basename, additional_param=layout) self.file_name = file_name def get_title(self, obj): """get project title""" return obj.title def get_values(self, obj): """get label and shape for classes. The label contains all attributes and methods """ label = obj.title if obj.shape == "interface": label = "«interface»\\n%s" % label if not self.config.only_classnames: label = r"%s|%s\l|" % (label, r"\l".join(obj.attrs)) for func in obj.methods: if func.args.args: args = [arg.name for arg in func.args.args if arg.name != "self"] else: args = [] label = r"%s%s(%s)\l" % (label, func.name, ", ".join(args)) label = "{%s}" % label if is_exception(obj.node): return dict(fontcolor="red", label=label, shape="record") return dict(label=label, shape="record") def close_graph(self): """print the dot graph into """ self.printer.generate(self.file_name) class VCGWriter(DiagramWriter): """write vcg graphs from a diagram definition and a project """ def __init__(self, config): styles = [ dict(arrowstyle="solid", backarrowstyle="none", backarrowsize=0), dict(arrowstyle="solid", backarrowstyle="none", backarrowsize=10), dict( arrowstyle="solid", backarrowstyle="none", linestyle="dotted", backarrowsize=10, ), dict(arrowstyle="solid", backarrowstyle="none", textcolor="green"), ] DiagramWriter.__init__(self, config, styles) def set_printer(self, file_name, basename): """initialize VCGWriter for a UML graph""" self.graph_file = open(file_name, "w+") self.printer = VCGPrinter(self.graph_file) self.printer.open_graph( title=basename, layoutalgorithm="dfs", late_edge_labels="yes", port_sharing="no", manhattan_edges="yes", ) self.printer.emit_node = self.printer.node self.printer.emit_edge = self.printer.edge def get_title(self, obj): """get project title in vcg format""" return r"\fb%s\fn" % obj.title def get_values(self, obj): """get label and shape for classes. The label contains all attributes and methods """ if is_exception(obj.node): label = r"\fb\f09%s\fn" % obj.title else: label = r"\fb%s\fn" % obj.title if obj.shape == "interface": shape = "ellipse" else: shape = "box" if not self.config.only_classnames: attrs = obj.attrs methods = [func.name for func in obj.methods] # box width for UML like diagram maxlen = max(len(name) for name in [obj.title] + methods + attrs) line = "_" * (maxlen + 2) label = r"%s\n\f%s" % (label, line) for attr in attrs: label = r"%s\n\f08%s" % (label, attr) if attrs: label = r"%s\n\f%s" % (label, line) for func in methods: label = r"%s\n\f10%s()" % (label, func) return dict(label=label, shape=shape) def close_graph(self): """close graph and file""" self.printer.close_graph() self.graph_file.close()