#!/usr/bin/python3
# -*- coding: utf-8 -*-
# pylint: disable=line-too-long
# kate: space-indent on; indent-width 4; replace-tabs on; indent-mode python; remove-trailing-space modified;
# vim: expandtab ts=4
# pylint: enable=line-too-long
############################################################################
# Copyright © 2017-2020 José Manuel Santamaría Lema <panfaust@gmail.com> #
# #
# This program is free software; you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation; either version 2 of the License, or #
# (at your option) any later version. #
############################################################################
"""
This module just provides the KAGraph class.
"""
import time
import subprocess
import tempfile
import pygraphviz as pgv
[docs]
class KAGraph:
"""
Generic graph class, if you feed in packages and edges you will be able
to dump this graph to a dot, ps or pdf file and/or view the graph with
a pdf viewer.
"""
def __init__(self, title=''):
self._pgv_graph = pgv.AGraph(directed=True, strict=False, name=title)
self._pgv_graph.graph_attr['rankdir'] = 'LR'
self._pgv_graph.node_attr['style'] = 'filled'
self._pgv_graph.add_node('GOD', label='', width='0', fixedsize='true', style='invis')
self._title = title
self._pgv_graph.graph_attr['title'] = title
self._date_string = ''
self._package_status_list = []
self._arch_list = None # this means all architectures by default
self._expected_version = ""
self._different_versions = []
def _update_label(self):
#Show the label on top
self._pgv_graph.graph_attr['labelloc'] = 't'
#Set the label string with the title and the timestamp if any
self._pgv_graph.graph_attr['label'] = (
'< <FONT FACE="boldfontname" POINT-SIZE="40">%s</FONT> <BR/>'
'<FONT POINT-SIZE="30">Generated on %s </FONT> >'
% (self._title, self._date_string))
[docs]
def get_pgv_graph(self):
"""
Return the internal pygraphviz object.
"""
return self._pgv_graph
[docs]
def set_expected_version(self, version):
"""
Set the expected version for most of the packages in the graph.
"""
self._expected_version = version
[docs]
def set_different_versions(self, diff_version_list):
"""
Set the list of packages with different versions.
"""
self._different_versions = diff_version_list
[docs]
def set_arch_list(self, arch_list_str):
"""
Set architecture list, `arch_list_str` is a string like "amd64,i386".
"""
if arch_list_str == 'any':
self._arch_list = None
else:
self._arch_list = arch_list_str.split(',')
[docs]
def add_package(self, package_name):
"""
Add a package (node) to the graph.
"""
self._pgv_graph.add_node(package_name)
self._pgv_graph.add_edge('GOD', package_name, style='invis')
[docs]
def add_edge(self, package1, package2):
"""
Add an arrow (edge) from `package1` to `package2`.
"""
self._pgv_graph.add_edge(package1, package2)
[docs]
def transitive_reduction(self):
"""
Perform transitive reduction.
"""
self._pgv_graph.tred()
[docs]
def set_package_status_list(self, status_list):
"""
Set the package status list.
"""
self._package_status_list = status_list
[docs]
def set_package_status(self, package, status):
"""
Set the status of the given package (node).
"""
try:
node = self._pgv_graph.get_node(package)
except KeyError:
raise KeyError("Package %s not found in graph" % package)
node.attr['fillcolor'] = status.color
node.attr['href'] = status.url
[docs]
def set_package_url(self, package, url):
"""
Set an url clickable link for a specific package (node).
"""
try:
node = self._pgv_graph.get_node(package)
except KeyError:
raise KeyError("Package %s not found in graph" % package)
node.attr['href'] = url
[docs]
def populate_from_bd_relations_map(self, bd_relations_map):
"""
Populate the graph with `bd_relations_map`.
"""
for package in sorted(bd_relations_map):
self.add_package(package)
build_depends = bd_relations_map[package]
for build_depend in sorted(build_depends):
self.add_edge(build_depend, package)
[docs]
def highlight_package(self, package):
"""
Highlight a package (node) of the graph drawing a triple octagon around it.
"""
try:
node = self._pgv_graph.get_node(package)
except KeyError:
raise KeyError("Package %s not found in graph" % package)
#Highlight the package changing its shape
node.attr['shape'] = 'tripleoctagon'
node.attr['style'] = 'bold,rounded,filled'
[docs]
def add_legend(self):
"""
Add box with the graph node status legend.
"""
#Create the cluster with the proper label and shape
subgraph = self._pgv_graph.add_subgraph('', 'cluster_legend')
subgraph.graph_attr['label'] = 'Legend'
subgraph.graph_attr['style'] = 'bold,rounded'
#Add a node for each possible status
for status in self._package_status_list:
if status.text:
node_name = status.text
else:
node_name = status.name
subgraph.add_node(node_name)
node = subgraph.get_node(node_name)
node.attr['fillcolor'] = status.color
node.attr['shape'] = 'plaintext'
self._pgv_graph.add_edge(node, 'GOD', style='invis')
[docs]
def update_timestamp(self):
"""
Update the graph label timestamp.
"""
self._date_string = time.strftime('%a, %d %b %Y %X %z')
self._update_label()
[docs]
def dump_graph_to_dot(self, file_name):
#pylint: disable=anomalous-backslash-in-string
r"""
Generate a \*.dot file with the graph.
"""
#pylint: enable=anomalous-backslash-in-string
self._pgv_graph.write(file_name)
[docs]
def dump_graph_to_ps(self, file_name):
"""
Generate a PS file with the graph.
"""
self._pgv_graph.draw(file_name, prog='dot', format='ps2')
[docs]
def dump_graph_to_pdf(self, file_name):
"""
Generate a PDF file with the graph.
"""
#Dump graph to a temporary PostScript 2 file
tmp_ps_file = tempfile.NamedTemporaryFile(delete=False)
self.dump_graph_to_ps(tmp_ps_file.name)
#Find out the pdf file name to write
#Convert the ps file to pdf; otherwise URL links won't work, see:
#https://www.graphviz.org/faq/#FaqPDF
#
# "If your version of Graphviz has cairo/pango support, you can just use the
# -Tpdf flag. Unfortunately, this does not handle embedded links.
#
# If you need embedded links, or don’t have cairo/pango, create PostScript output,
# then use an external converter from PostScript to PDF. For example,
# dot -Tps | epsf2pdf -o file.pdf. Note that URL tags are respected, to allow
# clickable PDF objects.
subprocess.check_call(['ps2pdf', tmp_ps_file.name, file_name])
[docs]
def display_graph(self, file_name=None):
"""
Display the graph in a pdf viewer.
"""
if file_name is None:
tmp_pdf_file = tempfile.NamedTemporaryFile(delete=False)
file_name = tmp_pdf_file.name
self.dump_graph_to_pdf(file_name)
subprocess.check_call(['okular', file_name])