From a700c950e25e2737f13f71d048c8a7cdc76d30ca Mon Sep 17 00:00:00 2001 From: Tiago Freitas Pereira <tiagofrepereira@gmail.com> Date: Fri, 1 Nov 2019 07:55:07 +0100 Subject: [PATCH] Implemented a mechanism to run a dependency graph --- bob/devtools/graph.py | 120 ++++++++++++++++++++++++++++++++++ bob/devtools/scripts/graph.py | 45 +++++++++++++ conda/meta.yaml | 2 + setup.py | 1 + 4 files changed, 168 insertions(+) create mode 100644 bob/devtools/graph.py create mode 100644 bob/devtools/scripts/graph.py diff --git a/bob/devtools/graph.py b/bob/devtools/graph.py new file mode 100644 index 00000000..70077ef2 --- /dev/null +++ b/bob/devtools/graph.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import conda.cli.python_api +import json + +from .log import verbosity_option, get_logger, echo_info + +logger = get_logger(__name__) + + +from graphviz import Digraph + + +def get_graphviz_dependency_graph( + graph_dict, + file_name, + prefix="bob.", + black_list=["python", "setuptools", "libcxx", "numpy", "libblitz", "boost"], +): + """ + Given a dictionary with the dependency graph, compute the graphviz DAG and save it + in SVG + """ + + d = Digraph(format="svg", engine="dot") + + for i in graph_dict: + for j in graph_dict[i]: + # Conections to python, setuptools....gets very messy + if j in black_list: + continue + + if prefix in j: + d.attr("node", shape="box") + else: + d.attr("node", shape="ellipse") + d.edge(i, j) + d.render(file_name) + + +def compute_dependency_graph( + package_name, channel=None, selected_packages=[], prefix="bob.", dependencies=dict() +): + """ + Given a target package, returns an adjacency matrix with its dependencies returned via the command `conda search xxxx --info` + + **Parameters** + + package_name: + Name of the package + + channel: + Name of the channel to be sent via `-c` option. If None `conda search` will use what is in .condarc + + selected_packages: + List of target packages. If set, the returned adjacency matrix will be in terms of this list. + + prefix: + Only seach for deep dependencies under the prefix. This would avoid to go deeper in + dependencies not maintained by us, such as, numpy, matplotlib, etc.. + + dependencies: + Dictionary controlling the state of each search + + """ + + if package_name in dependencies: + return dependencies + + dependencies[package_name] = fetch_dependencies( + package_name, channel, selected_packages + ) + logger.info(f" >> Searching dependencies of {package_name}") + for d in dependencies[package_name]: + if prefix in d: + compute_dependency_graph( + d, channel, selected_packages, prefix, dependencies + ) + return dependencies + + +def fetch_dependencies(package_name, channel=None, selected_packages=[]): + """ + conda search the dependencies of a package + + **Parameters** + packge_name: + channel: + selected_packages: + """ + + # Running conda search and returns to a json file + if channel is None: + package_description = conda.cli.python_api.run_command( + conda.cli.python_api.Commands.SEARCH, package_name, "--info", "--json" + ) + else: + package_description = conda.cli.python_api.run_command( + conda.cli.python_api.Commands.SEARCH, + package_name, + "--info", + "-c", + channel, + "--json", + ) + + # TODO: Fix that + package_description = json.loads(package_description[0]) + + # Fetching the dependencies of the most updated package + all_dependencies = [ + p.split(" ")[0] for p in package_description[package_name][-1]["depends"] + ] + + if len(selected_packages) > 0: + # Filtering the dependencies + return [d for d in selected_packages if d in all_dependencies] + + return all_dependencies diff --git a/bob/devtools/scripts/graph.py b/bob/devtools/scripts/graph.py new file mode 100644 index 00000000..3415eacb --- /dev/null +++ b/bob/devtools/scripts/graph.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import click +from click_plugins import with_plugins + +from ..graph import compute_dependency_graph, get_graphviz_dependency_graph + +from ..log import verbosity_option, get_logger, echo_info + +logger = get_logger(__name__) + + +@click.command( + epilog=""" +Example: + + bdt graph bob.bio.face graph + + +""" +) +@click.argument("package_name", required=True) +@click.argument("output_file", required=True) +@click.option( + "-c", + "--channel", + default=None, + help="Define a target channel for conda serch. If not set, will use what is set in .condarc", +) +@click.option( + "-p", + "--prefix", + default="bob.", + help="It will recursivelly look into dependencies whose package name matches the prefix. Default 'bob.'", +) +@verbosity_option() +def graph(package_name, output_file, channel, prefix): + """ + Compute the dependency graph of a conda package and save it in an SVG file using graphviz. + """ + logger.info(f"Computing dependency graph") + graph_dict = compute_dependency_graph(package_name, channel=channel, prefix=prefix) + logger.info("Generating SVG") + get_graphviz_dependency_graph(graph_dict, output_file, prefix=prefix) diff --git a/conda/meta.yaml b/conda/meta.yaml index 43c37974..40ea6b0a 100644 --- a/conda/meta.yaml +++ b/conda/meta.yaml @@ -49,6 +49,7 @@ requirements: - termcolor - psutil - tabulate + - python-graphviz test: requires: @@ -104,6 +105,7 @@ test: - bdt dav upload --help - bdt gitlab process-pipelines --help - bdt gitlab get-pipelines --help + - bdt graph --help - sphinx-build -aEW ${PREFIX}/share/doc/{{ name }}/doc sphinx - if [ -n "${CI_PROJECT_DIR}" ]; then mv sphinx "${CI_PROJECT_DIR}/"; fi diff --git a/setup.py b/setup.py index 1e729138..87e08fd0 100644 --- a/setup.py +++ b/setup.py @@ -56,6 +56,7 @@ setup( 'dav = bob.devtools.scripts.dav:dav', 'local = bob.devtools.scripts.local:local', 'gitlab = bob.devtools.scripts.gitlab:gitlab', + 'graph = bob.devtools.scripts.graph:graph' ], 'bdt.gitlab.cli': [ -- GitLab