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 |
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()