dev-pipeline/dev-pipeline-build-order

View on GitHub
lib/devpipeline_buildorder/dot.py

Summary

Maintainability
A
3 hrs
Test Coverage
#!/usr/bin/python3

"""
Output methods that use dot syntax to represent dependency information.
"""

import re
import sys

import devpipeline_core.resolve


def _dotify(string):
    """This function swaps '-' for '_'."""
    return re.sub("[-/]", lambda m: "_", string)


def _do_dot(targets, components, tasks, layer_fn):
    print("digraph dependencies {")
    try:

        def _make_attribute_fn():
            if len(tasks) > 1:
                return lambda component_task: ' [label="{}"]'.format(component_task[1])
            return lambda component_task: ""

        def _print_dependency(
            indentation, component_task, dependent_task, attribute_fn
        ):
            if component_task[0] != dependent_task[0]:
                print(
                    "{}{} -> {}{}".format(
                        indentation,
                        _dotify(component_task[0]),
                        _dotify(dependent_task[0]),
                        attribute_fn(component_task),
                    )
                )

        attribute_fn = _make_attribute_fn()
        dm = devpipeline_core.resolve.calculate_dependencies(targets, components, tasks)

        def _print_dependencies(indentation, component_tasks):
            for component_task in component_tasks:
                dependencies = dm.get_dependencies(component_task)
                if dependencies:
                    for dependency_task in dependencies:
                        _print_dependency(
                            indentation, component_task, dependency_task, attribute_fn
                        )
                else:
                    print("{}{}".format(indentation, _dotify(component_task[0])))

        task_queue = dm.get_queue()
        for component_tasks in task_queue:
            layer_fn(
                component_tasks,
                lambda indentation, resolved: _print_dependencies(
                    indentation, resolved
                ),
            )
            for component_task in component_tasks:
                task_queue.resolve(component_task)
    except devpipeline_core.resolve.CircularDependencyException as cde:
        del cde
    print("}")


def _print_layers(targets, components, tasks):
    """
    Print dependency information, grouping components based on their position
    in the dependency graph.  Components with no dependnecies will be in layer
    0, components that only depend on layer 0 will be in layer 1, and so on.

    If there's a circular dependency, those nodes and their dependencies will
    be colored red.

    Arguments
    targets - the targets explicitly requested
    components - full configuration for all components in a project
    """
    layer = 0
    expected_count = len(tasks)
    counts = {}

    def _add_layer(resolved, dep_fn):
        nonlocal layer
        nonlocal counts
        nonlocal expected_count

        really_resolved = []
        for resolved_task in resolved:
            resolved_component_tasks = counts.get(resolved_task[0], [])
            resolved_component_tasks.append(resolved_task)
            if len(resolved_component_tasks) == expected_count:
                really_resolved.extend(resolved_component_tasks)
                del counts[resolved_task[0]]
            else:
                counts[resolved_task[0]] = resolved_component_tasks

        if really_resolved:
            indentation = " " * 4
            print("{}subgraph cluster_{} {{".format(indentation, layer))
            print('{}label="Layer {}"'.format(indentation * 2, layer))
            dep_fn(indentation * 2, really_resolved)
            print("{}}}".format(indentation))
            layer += 1

    _do_dot(targets, components, tasks, _add_layer)


_LAYERS_TOOL = (
    _print_layers,
    "Print a dot graph that groups components by their position in a layered "
    "architecture.  Components are only permitted to depend on layers with a "
    "lower number.",
)


def _print_graph(targets, components, tasks):
    """
    Print dependency information using a dot directed graph.  The graph will
    contain explicitly requested targets plus any dependencies.

    If there's a circular dependency, those nodes and their dependencies will
    be colored red.

    Arguments
    targets - the targets explicitly requested
    components - full configuration for all components in a project
    """
    indentation = " " * 4
    _do_dot(
        targets,
        components,
        tasks,
        lambda resolved, dep_fn: dep_fn(indentation, resolved),
    )


_GRAPH_TOOL = (
    _print_graph,
    "Print a dot graph where each " "component points at its dependnet components.",
)


def _print_dot(targets, components, tasks):
    """
    Deprecated function; use print_graph.

    Arguments
    targets - the targets explicitly requested
    components - full configuration for all components in a project
    """
    print("Warning: dot option is deprecated.  Use graph instead.", file=sys.stderr)
    _print_graph(targets, components, tasks)


_DOT_TOOL = (_print_dot, 'Deprecated -- use the "graph" option instead.')