Skip to content

Expose tree report as standalone cbi-tree utility #167

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Apr 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions codebasin/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ def _main():
metavar="<report>",
action="append",
default=[],
choices=["all", "summary", "clustering", "duplicates", "files"],
choices=["all", "summary", "clustering", "duplicates"],
help=_help_string(
"Generate a report of the specified type:",
"- summary: code divergence information",
Expand Down Expand Up @@ -246,10 +246,6 @@ def report_enabled(name):
if report_enabled("summary"):
report.summary(setmap)

# Print files report
if report_enabled("files"):
report.files(codebase, state)

# Print clustering report
if report_enabled("clustering"):
basename = os.path.basename(args.analysis_file)
Expand Down
32 changes: 26 additions & 6 deletions codebasin/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -649,9 +649,11 @@ def insert(
def _print(
self,
node: Node,
depth: int = 0,
prefix: str = "",
connector: str = "",
fancy: bool = True,
levels: int = None,
):
"""
Recursive helper function to print all nodes in a FileTree.
Expand All @@ -669,7 +671,14 @@ def _print(

fancy: bool, default: True
Whether to use fancy formatting (including colors).

levels: int, optional
The maximum number of levels to print.
"""
# Skip this node and its children if we have hit the maximum depth.
if levels and depth > levels:
return []

if fancy:
dash = "\u2500"
cont = "\u251C"
Expand Down Expand Up @@ -722,23 +731,29 @@ def _print(
next_connector = cont
lines += self._print(
node.children[name],
depth + 1,
next_prefix,
next_connector,
fancy,
levels,
)

return lines

def write_to(self, stream: TextIO):
def write_to(self, stream: TextIO, levels: int = None):
"""
Write the FileTree to the specified stream.

Parameters
----------
stream: TextIO
The text stream to write to.

levels: int, optional
The maximum number of levels to print.
If 0, print only the top-level summary.
"""
lines = self._print(self.root, fancy=stream.isatty())
lines = self._print(self.root, fancy=stream.isatty(), levels=levels)
output = "\n".join(lines)
if not stream.isatty():
output = _strip_colors(output)
Expand All @@ -748,7 +763,10 @@ def write_to(self, stream: TextIO):
def files(
codebase: CodeBase,
state: ParserState | None = None,
*,
stream: TextIO = sys.stdout,
prune: bool = False,
levels: int = None,
):
"""
Produce a file tree representing the code base.
Expand Down Expand Up @@ -787,11 +805,13 @@ def files(
):
platform = frozenset(association[node])
setmap[platform] += node.num_lines
if prune:
# Prune unused files from the tree.
platforms = set().union(*setmap.keys())
if len(platforms) == 0:
continue
tree.insert(f, setmap)

print("", file=stream)
print(_heading("Files", stream), file=stream)

# Print a legend.
legend = []
legend += ["Legend:"]
Expand All @@ -814,4 +834,4 @@ def files(
print(legend, file=stream)

# Print the tree.
tree.write_to(stream)
tree.write_to(stream, levels=levels)
187 changes: 187 additions & 0 deletions codebasin/tree.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
#!/usr/bin/env python3
# Copyright (C) 2019-2024 Intel Corporation
# SPDX-License-Identifier: BSD-3-Clause

import argparse
import logging
import os
import sys

from codebasin import CodeBase, config, finder, report, util

# TODO: Refactor to avoid imports from __main__
from codebasin.__main__ import Formatter, _help_string, version

log = logging.getLogger("codebasin")


def _build_parser() -> argparse.ArgumentParser:
"""
Build argument parser.
"""
parser = argparse.ArgumentParser(
description="CBI Tree Tool " + version,
formatter_class=argparse.RawTextHelpFormatter,
add_help=False,
)
parser.add_argument(
"-h",
"--help",
action="help",
help=_help_string("Display help message and exit."),
)
parser.add_argument(
"--version",
action="version",
version=f"CBI Coverage Tool {version}",
help=_help_string("Display version information and exit."),
)
parser.add_argument(
"-x",
"--exclude",
dest="excludes",
metavar="<pattern>",
action="append",
default=[],
help=_help_string(
"Exclude files matching this pattern from the code base.",
"May be specified multiple times.",
is_long=True,
),
)
parser.add_argument(
"-p",
"--platform",
dest="platforms",
metavar="<platform>",
action="append",
default=[],
help=_help_string(
"Include the specified platform in the analysis.",
"May be specified multiple times.",
"If not specified, all platforms will be included.",
is_long=True,
),
)
parser.add_argument(
"--prune",
dest="prune",
action="store_true",
help=_help_string(
"Prune unused files from the tree.",
),
)
parser.add_argument(
"-L",
"--levels",
dest="levels",
metavar="<level>",
type=int,
help=_help_string(
"Print only the specified number of levels.",
is_long=True,
is_last=True,
),
)

parser.add_argument(
"analysis_file",
metavar="<analysis-file>",
help=_help_string(
"TOML file describing the analysis to be performed, "
+ "including the codebase and platform descriptions.",
is_last=True,
),
)

return parser


def _tree(args: argparse.Namespace):
# Refuse to print a tree with no levels, consistent with tree utility.
if args.levels is not None and args.levels <= 0:
raise ValueError("Number of levels must be greater than 0.")

# TODO: Refactor this to avoid duplication in __main__
# Determine the root directory based on where codebasin is run.
rootdir = os.path.abspath(os.getcwd())

# Set up a default configuration object.
configuration = {}

# Load the analysis file if it exists.
if args.analysis_file is not None:
path = os.path.abspath(args.analysis_file)
if os.path.exists(path):
if not os.path.splitext(path)[1] == ".toml":
raise RuntimeError(f"Analysis file {path} must end in .toml.")

with open(path, "rb") as f:
analysis_toml = util._load_toml(f, "analysis")

if "codebase" in analysis_toml:
if "exclude" in analysis_toml["codebase"]:
args.excludes += analysis_toml["codebase"]["exclude"]

for name in args.platforms:
if name not in analysis_toml["platform"].keys():
raise KeyError(
f"Platform {name} requested on the command line "
+ "does not exist in the configuration file.",
)

cmd_platforms = args.platforms.copy()
for name in analysis_toml["platform"].keys():
if cmd_platforms and name not in cmd_platforms:
continue
if "commands" not in analysis_toml["platform"][name]:
raise ValueError(f"Missing 'commands' for platform {name}")
p = analysis_toml["platform"][name]["commands"]
db = config.load_database(p, rootdir)
args.platforms.append(name)
configuration.update({name: db})

# Construct a codebase object associated with the root directory.
codebase = CodeBase(rootdir, exclude_patterns=args.excludes)

# Parse the source tree, and determine source line associations.
# The trees and associations are housed in state.
state = finder.find(
rootdir,
codebase,
configuration,
show_progress=True,
)

# Print the file tree.
report.files(codebase, state, prune=args.prune, levels=args.levels)
sys.exit(0)


def cli(argv: list[str]) -> int:
parser = _build_parser()
args = parser.parse_args(argv)

# Configure logging such that:
# - Only errors are written to the terminal
log.setLevel(logging.DEBUG)

stderr_handler = logging.StreamHandler(sys.stderr)
stderr_handler.setLevel(logging.ERROR)
stderr_handler.setFormatter(Formatter(colors=sys.stderr.isatty()))
log.addHandler(stderr_handler)

return _tree(args)


def main():
try:
cli(sys.argv[1:])
except Exception as e:
log.error(str(e))
sys.exit(1)


if __name__ == "__main__":
sys.argv[0] = "codebasin.tree"
main()
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ dependencies = [
[project.scripts]
codebasin = "codebasin:__main__.main"
cbi-cov = "codebasin.coverage:__main__.main"
cbi-tree = "codebasin:tree.main"

[project.urls]
"Github" = "https://www.github.com/intel/code-base-investigator"
Expand Down
Loading