diff --git a/bob/extension/scripts/__init__.py b/bob/extension/scripts/__init__.py index 60b3f6d43e89d1f4dfbe246105d1e47f1a23021c..4b9f68dec6a521b9d90c5e3f5b9f38a7e8561580 100644 --- a/bob/extension/scripts/__init__.py +++ b/bob/extension/scripts/__init__.py @@ -1,4 +1,5 @@ from .new_version import main as new_version +from .dependency_graph import main as dependency_graph # gets sphinx autodoc done right - don't remove it __all__ = [_ for _ in dir() if not _.startswith('_')] diff --git a/bob/extension/scripts/dependency_graph.py b/bob/extension/scripts/dependency_graph.py new file mode 100644 index 0000000000000000000000000000000000000000..792e1817d1f7e1c243e7e154ec765775aedde4a3 --- /dev/null +++ b/bob/extension/scripts/dependency_graph.py @@ -0,0 +1,127 @@ +#!../bin/python + + +#!/usr/bin/env python + +from __future__ import print_function +import subprocess +import pkg_resources +import tempfile, os + +import argparse + +def main(command_line_options = None): + """ + This script will generate a dependency graph of the given bob package(s) using the external tool ``dot``. + Packages can be either specified as a list of ``--packages`` or read from (several) ``--package-files``. + The latter option is mostly useful to generate a dot graph for all Bob packages. + + The output is written to the given ``--output-file``, writing either the specified intermediate ``--dot-file``, or a temporary file. + When the ``--plot-external-dependencies`` is selected, also external (Python-)dependencies will be plotted as well, in red ellipses. + """ + + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + + parser.add_argument("--packages", '-p', nargs = '+', default = [], help = "Which packages do you want to have dependencies for?") + parser.add_argument("--package-files", '-P', nargs = '+', default = [], help = "Read packages from the given files (usually requirement?.txt files of bob).") + parser.add_argument("--dot-file", "-W", help = "If specified, the .dot file is written to the given file.") + parser.add_argument("--output-file", "-w", default="dependencies.png", help = "Specify the (.png) file to write.") + parser.add_argument("--limit-packages", '-l', nargs = '+', default = ['bob', 'facereclib', 'antispoofing'], help = "Limit packages read from --package-files to the given namespaces") + parser.add_argument("--plot-external-dependencies", '-X', action='store_true', help = "Include external dependencies into the plot?") + parser.add_argument("--rank-base-tools-same", '-R', action = 'store_true', help = "Set the rank of packages bob.extension, bob.core and bob.blitz at the same size") + parser.add_argument("--vertical", '-V', action = 'store_true', help = "Display the dot graph in vertical direction") + parser.add_argument("--verbose", '-v', action = 'store_true', help = "Print more information") + + args = parser.parse_args(command_line_options) + + + # collect packages + packages = args.packages[:] + for package_file in args.package_files: + for line in open(package_file): + splits = line.rstrip().split() + packages.extend([p for p in splits if p not in packages and p.startswith(tuple(args.limit_packages))]) + + # generate dependencies + dependencies = {} + has_parents = set() + + # function to add dependencies of packages recursively + def _add_recursive(p): + # check if package already parsed + if p not in dependencies: + if args.verbose: + print("Checking %s" % p) + deps = pkg_resources.require(p) + dependencies[p] = [d.key for d in deps[1:]] + for d in dependencies[p]: + has_parents.add(d) + _add_recursive(d) + + for package in packages: + _add_recursive(package) + + # prune dependencies + pruned_dependencies = {} + for package in dependencies: + indirect_dependencies = set(d for i in [dependencies[dep] for dep in dependencies[package] if dep in dependencies] for d in i) + pruned_dependencies[package] = [dep for dep in dependencies[package] if dep not in indirect_dependencies] + + # split all dependencies that are from bob (i.e., that belong to the --limit-packages) or not + bob = set(package for package in pruned_dependencies if package.startswith(tuple(args.limit_packages))) + non_bob = set(dep for package in bob for dep in pruned_dependencies[package] if not dep.startswith(tuple(args.limit_packages))) + final = set(package for package in bob if package not in has_parents) + + + # function to return a name for the package that can serve as a dot variable + def _n(p): + return p.replace(".", "_").replace("-","_") + + # write dependency graph + dot_file = args.dot_file if args.dot_file is not None else tempfile.mkstemp(suffix='.dot')[1] + with open(dot_file, 'w') as f: + # open plot + f.write("digraph Bob {\n") + if not args.vertical: + f.write("\trankdir=LR;\n") + # write bob packages in squares + for package in bob: + f.write('\t%s [label="%s",shape=box,group=%s,style=filled,color=%s];\n' % (_n(package), package, "_".join(package.split('.')[:2]), 'green' if package in final else 'lightblue')) + + # write non-bob packages in squares + if args.plot_external_dependencies: + for package in non_bob: + f.write('\t%s [label="%s",color=red];\n' % (_n(package), package)) + + # write dependencies + for package in bob: + for dep in pruned_dependencies[package]: + if dep in bob: + f.write('\t%s -> %s [color=blue];\n' % (_n(package), _n(dep))) + elif args.plot_external_dependencies: + f.write('\t%s -> %s [color=red,style=dashed];\n' % (_n(package), _n(dep))) + + # rank all externals at the same level + if args.plot_external_dependencies: + f.write('\t{rank=same; %s }\n' % " ".join([_n(p) for p in non_bob])) + # rank base tools at the same level + if args.rank_base_tools_same: + f.write('\t{rank=same; bob_extension bob_core bob_blitz}\n') + f.write("}\n") + + if args.verbose and args.dot_file is not None: + print("Wrote dot file %s" % dot_file) + + # call dot + call = ["dot", '-y', '-o', args.output_file, '-Tpng:cairo:gd', dot_file] + if args.verbose: + call[1:1] = ['-v'] + print("Calling dot: '%s'" % " ".join(call)) + subprocess.call(call) + + if args.verbose: + print("\nWrote file %s" % args.output_file) + + # clean-up + if args.dot_file is None: + os.remove(dot_file) diff --git a/setup.py b/setup.py index 88d940683f273e6e8d23b8b65cc88383e94bd2e0..5c389e3d37f5d8b88da7ca44e0fb7fd03cf786a8 100644 --- a/setup.py +++ b/setup.py @@ -40,6 +40,7 @@ setup( entry_points = { 'console_scripts': [ 'bob_new_version.py = bob.extension.scripts:new_version', + 'bob_dependecy_graph.py = bob.extension.scripts:dependency_graph', ], },