Skip to content

viewer

formatter

format_dot(graph, results, query, query_value)

Output result as DOT.

Parameters:

Name Type Description Default
graph DiGraph

BGraph

required
results List[str]

Results for the query

required
query QueryType

Type of the query

required
query_value str

Query value

required
Source code in bgraph/viewer/formatter.py
def format_dot(
    graph: BGraph, results: List[str], query: QueryType, query_value: str
) -> None:
    """Output result as DOT.

    :param graph: BGraph
    :param results: Results for the query
    :param query: Type of the query
    :param query_value: Query value
    """

    if query == QueryType.SOURCE:
        subgraph = networkx.generators.ego_graph(
            graph, query_value, center=True, radius=None
        )
    elif query == QueryType.TARGET:
        subgraph = networkx.generators.ego_graph(
            graph.reverse(), query_value, center=True, radius=None
        )
    elif query == QueryType.DEPENDENCY:
        subgraph = graph.subgraph(results)
    else:
        raise NotImplementedError("Not implemented yet")

    pydot_graph = networkx.nx_pydot.to_pydot(subgraph)

    # Clean data
    for node in pydot_graph.get_nodes():
        try:
            del node.obj_dict["attributes"]["data"]
        except KeyError:
            pass

    try:
        target = pydot_graph.get_node(pydot.quote_if_necessary(query_value))[0]
    except IndexError:
        raise bgraph.exc.BGraphNodeNotFound("Unable to find node")

    target.set_shape("box")
    target.set_color("red")

    typer.echo(pydot_graph)

format_json(graph, results, query, query_value)

Output the result as JSON

Parameters:

Name Type Description Default
graph DiGraph

BGraph

required
results List[str]

Results for the query

required
query QueryType

Type of the query

required
query_value str

Query value

required
Source code in bgraph/viewer/formatter.py
def format_json(
    graph: BGraph, results: List[str], query: QueryType, query_value: str
) -> None:
    """Output the result as JSON

    :param graph: BGraph
    :param results: Results for the query
    :param query: Type of the query
    :param query_value: Query value
    """

    query_dict = {
        QueryType.TARGET: ["target", "Search sources for a target"],
        QueryType.SOURCE: ["sources", "Search target for a source"],
        QueryType.DEPENDENCY: ["dependency", "Search dependencies for a target"],
    }

    result_dict: ResultDict = {}
    if query == QueryType.TARGET:
        result_dict["sources"] = results
    elif query == QueryType.DEPENDENCY or query == QueryType.SOURCE:
        result_dict["target"] = []
        for result in results:
            result_dict["target"].append(
                (result, bgraph.viewer.get_node_type(graph.nodes[result]))
            )

    typer.echo(
        json.dumps(
            {
                "_meta": {
                    "query": query_dict[query][0],
                    "desc": query_dict[query][1],
                    "query_value": query_value,
                },
                "result": result_dict,
            },
            indent=True,
        )
    )

format_result(graph, results, query, query_value, out_choice)

Format the result of the viewer.

Parameters:

Name Type Description Default
graph DiGraph

BGraph

required
results List[str]

Results for the query

required
query QueryType

Type of the query

required
query_value str

Query value

required
out_choice OutChoice

Out format

required
Source code in bgraph/viewer/formatter.py
def format_result(
    graph: BGraph,
    results: List[str],
    query: QueryType,
    query_value: str,
    out_choice: OutChoice,
) -> None:
    """Format the result of the viewer.

    :param graph: BGraph
    :param results: Results for the query
    :param query: Type of the query
    :param query_value: Query value
    :param out_choice: Out format
    """

    format_methods = {
        out_choice.TXT: format_text,
        out_choice.JSON: format_json,
        out_choice.DOT: format_dot,
    }

    return format_methods[out_choice](graph, results, query, query_value)

format_text(graph, results, query, query_value)

Format the results for text consumption (default value).

Use rich to do some pretty formatting.

Parameters:

Name Type Description Default
graph DiGraph

BGraph

required
results List[str]

Results for the query

required
query QueryType

Type of the query

required
query_value str

Query value

required
Source code in bgraph/viewer/formatter.py
def format_text(
    graph: BGraph, results: List[str], query: QueryType, query_value: str
) -> None:
    """Format the results for text consumption (default value).

    Use rich to do some pretty formatting.

    :param graph: BGraph
    :param results: Results for the query
    :param query: Type of the query
    :param query_value: Query value
    """
    table = rich.table.Table(box=rich.box.MINIMAL_DOUBLE_HEAD)

    if query == QueryType.TARGET:
        table.title = f"Sources for the target {query_value}"
        table.add_column("Filename", justify="left")
        table.add_column("File type", justify="right")

        for result in results:
            table.add_row(result, Path(result).suffix)

    elif query == QueryType.DEPENDENCY:
        table.title = f"Dependencies for the target {query_value}"
        table.add_column("Dependency")
        table.add_column("Type")
        table.add_column("Ascending")

        for result in sorted(results):

            ascending = (
                ":heavy_check_mark:"
                if networkx.has_path(graph, result, query_value)
                else ":heavy_multiplication_x:"
            )
            table.add_row(
                result, bgraph.viewer.get_node_type(graph.nodes[result]), ascending
            )

    elif query == QueryType.SOURCE:
        table.title = f"Dependencies for source file {Path(query_value).name}"
        table.add_column("Target")
        table.add_column("Type")
        table.add_column("Distance")

        # Generate the graph to compute the distance
        # Since the graph is much simpler than the original one, it is easier to compute
        # the distance inside this one.
        generated_graph: BGraph = networkx.generators.ego_graph(
            graph, query_value, center=True, radius=None
        )

        row_results: List[Tuple[str, NodeType, int]] = [
            (
                result,
                bgraph.viewer.get_node_type(graph.nodes[result]),
                networkx.algorithms.shortest_path_length(
                    generated_graph, query_value, result
                ),
            )
            for result in results
        ]

        for result, node_type, distance in sorted(row_results, key=lambda x: x[2]):
            table.add_row(result, node_type, f"{distance}")

    console = rich.console.Console()
    console.print(table)

loader

load_graph(graph_path)

Load a B-Graph and return the DiGraph associated.

Parameters:

Name Type Description Default
graph

Path to the graph file (stored with pickle)

required

Returns:

Type Description
DiGraph

A DiGraph

Source code in bgraph/viewer/loader.py
def load_graph(graph_path: Union[str, pathlib.Path]) -> BGraph:
    """Load a B-Graph and return the DiGraph associated.

    :param graph: Path to the graph file (stored with pickle)
    :return: A DiGraph
    """
    try:
        graph: BGraph = pickle.load(open(graph_path, "rb"))
    except (pickle.PickleError, FileNotFoundError):
        raise bgraph.exc.BGraphLoadingException("Unable to load the graph.")

    return graph

viewer

DEFAULT_TYPES: List[str]

Default soong types to consider

logger: Logger

Logger.

find_dependency(graph, origin)

Resolve dependencies in a graph.

Given an origin (which is not a source file), find all dependents targets

Parameters:

Name Type Description Default
graph DiGraph

Graph to search

required
origin str

Origin of the query

required

Returns:

Type Description
List[str]

A list of dependent target

Source code in bgraph/viewer/viewer.py
def find_dependency(graph: BGraph, origin: str) -> List[str]:
    """Resolve dependencies in a graph.

    Given an origin (which is *not* a source file), find all dependents targets

    :param graph: Graph to search
    :param origin: Origin of the query
    :return: A list of dependent target
    """

    if origin not in graph:
        logger.error("Origin not found %s", origin)
        return []

    # Get dependencies in the graph
    subgraph = nx.generators.ego_graph(graph, origin, radius=None, center=True)
    other_subgraph = nx.generators.ego_graph(
        graph.reverse(), origin, center=True, radius=None
    )

    return list(set(subgraph).union(set(other_subgraph)))

find_sources(graph, target)

Find the sources of target.

Recursively in the graph, search for all sources files of a target (or a target dependencies).

TODO(dm): For conditionals, there may also exists precomputed binaries as the target. Find a way to deal with those

Parameters:

Name Type Description Default
graph DiGraph

Graph yo search

required
target str

Origin of the query (final target)

required

Returns:

Type Description
List[str]

A list of source files

Source code in bgraph/viewer/viewer.py
def find_sources(graph: BGraph, target: str) -> List[str]:
    """Find the sources of target.

    Recursively in the graph, search for all sources files of a target (or a target
     dependencies).

    TODO(dm):
        For conditionals, there may also exists precomputed binaries as the target.
        Find a way to deal with those

    :param graph: Graph yo search
    :param target: Origin of the query (final target)
    :return: A list of source files
    """
    if target not in graph:
        return []

    subgraph = nx.generators.ego_graph(
        graph.reverse(), target, radius=None, center=False
    )
    dependencies = [
        node for node in subgraph if next(subgraph.successors(node), None) is None
    ]

    # Filtering step: since we don't understand conditionals (yet), filter out bogus
    # dependencies
    # TODO(dm)

    return dependencies

find_target(graph, source, return_types=['cc_library_shared', 'cc_library', 'cc_binary', 'cc_library_static', 'android_app'], radius=None)

Given a source file, find all dependent targets.

This is a bit trickier as the source file may be given with an incomplete path. However, we don't want to give absurds results, so if more than 1 file matches, an error is raised.

TODO(dm): - Intersect for multiple sources files - Better handling of return types

Parameters:

Name Type Description Default
graph DiGraph

The graph to search

required
source str

Source file name

required
return_types List[str]

Optional. List of types to consider as valid types

['cc_library_shared', 'cc_library', 'cc_binary', 'cc_library_static', 'android_app']
radius Optional[int]

Optional. How far should the graph go. Default is None : consider all the dependencies. A positive integer will reduce to node at at most radius distance.

None

Returns:

Type Description
Tuple[str, List[str]]

A tuple with the exact match and the list of results

Source code in bgraph/viewer/viewer.py
def find_target(
    graph: BGraph,
    source: str,
    return_types: List[str] = DEFAULT_TYPES,
    radius: Optional[int] = None,
) -> Tuple[str, List[str]]:
    """Given a source file, find all dependent targets.

    This is a bit trickier as the source file may be given with an incomplete path.
    However, we don't want to give absurds results, so if more than 1 file matches, an
    error is raised.

    TODO(dm):
        - Intersect for multiple sources files
        - Better handling of return types

    :param graph: The graph to search
    :param source: Source file name
    :param return_types: Optional. List of types to consider as valid types
    :param radius: Optional. How far should the graph go.
        Default is None : consider all the dependencies. A positive integer will reduce
        to node at at most `radius` distance.
    :return: A tuple with the exact match and the list of results
    """
    graph_srcs: List[str] = get_graph_srcs(graph)
    try:
        matched_node = match_node(graph_srcs, source)
    except (bgraph.exc.BGraphNodeNotFound, bgraph.exc.BGraphTooManyNodes) as e:
        logger.info("Failed to find node with error %s", e)
        return "", []

    subgraph = nx.generators.ego_graph(graph, matched_node, center=False, radius=radius)
    results = [
        node
        for node in subgraph
        if any(
            node_type in return_types
            for node_type in get_node_type(graph.nodes[node], all_types=True)
        )
    ]

    return matched_node, results

get_graph_srcs(graph)

Filter the graph to return only source nodes.

This method is used to improve the efficiency of the match_node method.

Parameters:

Name Type Description Default
graph DiGraph

The BGraph to filter

required

Returns:

Type Description
List[str]

A list of graph nodes representing source file.

Source code in bgraph/viewer/viewer.py
@functools.lru_cache(maxsize=8)
def get_graph_srcs(graph: BGraph) -> List[str]:
    """Filter the graph to return only source nodes.

    This method is used to improve the efficiency of the `match_node` method.

    :param graph: The BGraph to filter
    :return: A list of graph nodes representing source file.
    """
    return [node for node in graph if get_node_type(node) == "source"]

get_node_type(node_d, all_types=False)

Get the node type

Sources nodes (e.g leaves) have no data associated so we use this fact.

Source code in bgraph/viewer/viewer.py
def get_node_type(
    node_d: Dict, all_types: bool = False
) -> Union[NodeType, List[NodeType]]:
    """Get the node type

    Sources nodes (e.g leaves) have no data associated so we use this fact.

    :param node_d A node
    :param all_types Optional. Return all the types possible for the node
    :return Type(s) of the node
    """
    try:
        node_types = [
            node[bgraph.parsers.SoongParser.SECTION_TYPE] for node in node_d["data"]
        ]
    except (TypeError, KeyError):
        return "source" if all_types is False else ["source"]

    if all_types:
        return node_types
    else:
        # Return only the first node type
        return node_types.pop()

match_node(graph_srcs, node_name)

Search for a node matching the name given as an argument.

Parameters:

Name Type Description Default
graph_srcs List[str]

A list of source node in the graph

required
node_name str

A node name

required

Returns:

Type Description
str

A node

Source code in bgraph/viewer/viewer.py
def match_node(graph_srcs: List[str], node_name: str) -> str:
    """Search for a node matching the name given as an argument.

    :param graph_srcs: A list of source node in the graph
    :param node_name: A node name
    :return: A node
    """

    potential_results = [node for node in graph_srcs if node_name in node]

    if not potential_results:
        raise bgraph.exc.BGraphNodeNotFound("Found 0 results")

    elif len(potential_results) > 1:
        # TODO(dm) : We have a problem here because we have too many nodes that may
        #  match but it is not supposed to happen.
        raise bgraph.exc.BGraphTooManyNodes("Found many results - refine the search")

    return potential_results.pop()