Skip to content

Commit 36a4b39

Browse files
authored
Merge pull request #167 from Pennycook/cbi-tree
Expose tree report as standalone cbi-tree utility
2 parents a5c34b9 + 2f4b32d commit 36a4b39

File tree

5 files changed

+324
-12
lines changed

5 files changed

+324
-12
lines changed

codebasin/__main__.py

+1-5
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ def _main():
9696
metavar="<report>",
9797
action="append",
9898
default=[],
99-
choices=["all", "summary", "clustering", "duplicates", "files"],
99+
choices=["all", "summary", "clustering", "duplicates"],
100100
help=_help_string(
101101
"Generate a report of the specified type:",
102102
"- summary: code divergence information",
@@ -246,10 +246,6 @@ def report_enabled(name):
246246
if report_enabled("summary"):
247247
report.summary(setmap)
248248

249-
# Print files report
250-
if report_enabled("files"):
251-
report.files(codebase, state)
252-
253249
# Print clustering report
254250
if report_enabled("clustering"):
255251
basename = os.path.basename(args.analysis_file)

codebasin/report.py

+26-6
Original file line numberDiff line numberDiff line change
@@ -649,9 +649,11 @@ def insert(
649649
def _print(
650650
self,
651651
node: Node,
652+
depth: int = 0,
652653
prefix: str = "",
653654
connector: str = "",
654655
fancy: bool = True,
656+
levels: int = None,
655657
):
656658
"""
657659
Recursive helper function to print all nodes in a FileTree.
@@ -669,7 +671,14 @@ def _print(
669671
670672
fancy: bool, default: True
671673
Whether to use fancy formatting (including colors).
674+
675+
levels: int, optional
676+
The maximum number of levels to print.
672677
"""
678+
# Skip this node and its children if we have hit the maximum depth.
679+
if levels and depth > levels:
680+
return []
681+
673682
if fancy:
674683
dash = "\u2500"
675684
cont = "\u251C"
@@ -722,23 +731,29 @@ def _print(
722731
next_connector = cont
723732
lines += self._print(
724733
node.children[name],
734+
depth + 1,
725735
next_prefix,
726736
next_connector,
727737
fancy,
738+
levels,
728739
)
729740

730741
return lines
731742

732-
def write_to(self, stream: TextIO):
743+
def write_to(self, stream: TextIO, levels: int = None):
733744
"""
734745
Write the FileTree to the specified stream.
735746
736747
Parameters
737748
----------
738749
stream: TextIO
739750
The text stream to write to.
751+
752+
levels: int, optional
753+
The maximum number of levels to print.
754+
If 0, print only the top-level summary.
740755
"""
741-
lines = self._print(self.root, fancy=stream.isatty())
756+
lines = self._print(self.root, fancy=stream.isatty(), levels=levels)
742757
output = "\n".join(lines)
743758
if not stream.isatty():
744759
output = _strip_colors(output)
@@ -748,7 +763,10 @@ def write_to(self, stream: TextIO):
748763
def files(
749764
codebase: CodeBase,
750765
state: ParserState | None = None,
766+
*,
751767
stream: TextIO = sys.stdout,
768+
prune: bool = False,
769+
levels: int = None,
752770
):
753771
"""
754772
Produce a file tree representing the code base.
@@ -787,11 +805,13 @@ def files(
787805
):
788806
platform = frozenset(association[node])
789807
setmap[platform] += node.num_lines
808+
if prune:
809+
# Prune unused files from the tree.
810+
platforms = set().union(*setmap.keys())
811+
if len(platforms) == 0:
812+
continue
790813
tree.insert(f, setmap)
791814

792-
print("", file=stream)
793-
print(_heading("Files", stream), file=stream)
794-
795815
# Print a legend.
796816
legend = []
797817
legend += ["Legend:"]
@@ -814,4 +834,4 @@ def files(
814834
print(legend, file=stream)
815835

816836
# Print the tree.
817-
tree.write_to(stream)
837+
tree.write_to(stream, levels=levels)

codebasin/tree.py

+187
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
#!/usr/bin/env python3
2+
# Copyright (C) 2019-2024 Intel Corporation
3+
# SPDX-License-Identifier: BSD-3-Clause
4+
5+
import argparse
6+
import logging
7+
import os
8+
import sys
9+
10+
from codebasin import CodeBase, config, finder, report, util
11+
12+
# TODO: Refactor to avoid imports from __main__
13+
from codebasin.__main__ import Formatter, _help_string, version
14+
15+
log = logging.getLogger("codebasin")
16+
17+
18+
def _build_parser() -> argparse.ArgumentParser:
19+
"""
20+
Build argument parser.
21+
"""
22+
parser = argparse.ArgumentParser(
23+
description="CBI Tree Tool " + version,
24+
formatter_class=argparse.RawTextHelpFormatter,
25+
add_help=False,
26+
)
27+
parser.add_argument(
28+
"-h",
29+
"--help",
30+
action="help",
31+
help=_help_string("Display help message and exit."),
32+
)
33+
parser.add_argument(
34+
"--version",
35+
action="version",
36+
version=f"CBI Coverage Tool {version}",
37+
help=_help_string("Display version information and exit."),
38+
)
39+
parser.add_argument(
40+
"-x",
41+
"--exclude",
42+
dest="excludes",
43+
metavar="<pattern>",
44+
action="append",
45+
default=[],
46+
help=_help_string(
47+
"Exclude files matching this pattern from the code base.",
48+
"May be specified multiple times.",
49+
is_long=True,
50+
),
51+
)
52+
parser.add_argument(
53+
"-p",
54+
"--platform",
55+
dest="platforms",
56+
metavar="<platform>",
57+
action="append",
58+
default=[],
59+
help=_help_string(
60+
"Include the specified platform in the analysis.",
61+
"May be specified multiple times.",
62+
"If not specified, all platforms will be included.",
63+
is_long=True,
64+
),
65+
)
66+
parser.add_argument(
67+
"--prune",
68+
dest="prune",
69+
action="store_true",
70+
help=_help_string(
71+
"Prune unused files from the tree.",
72+
),
73+
)
74+
parser.add_argument(
75+
"-L",
76+
"--levels",
77+
dest="levels",
78+
metavar="<level>",
79+
type=int,
80+
help=_help_string(
81+
"Print only the specified number of levels.",
82+
is_long=True,
83+
is_last=True,
84+
),
85+
)
86+
87+
parser.add_argument(
88+
"analysis_file",
89+
metavar="<analysis-file>",
90+
help=_help_string(
91+
"TOML file describing the analysis to be performed, "
92+
+ "including the codebase and platform descriptions.",
93+
is_last=True,
94+
),
95+
)
96+
97+
return parser
98+
99+
100+
def _tree(args: argparse.Namespace):
101+
# Refuse to print a tree with no levels, consistent with tree utility.
102+
if args.levels is not None and args.levels <= 0:
103+
raise ValueError("Number of levels must be greater than 0.")
104+
105+
# TODO: Refactor this to avoid duplication in __main__
106+
# Determine the root directory based on where codebasin is run.
107+
rootdir = os.path.abspath(os.getcwd())
108+
109+
# Set up a default configuration object.
110+
configuration = {}
111+
112+
# Load the analysis file if it exists.
113+
if args.analysis_file is not None:
114+
path = os.path.abspath(args.analysis_file)
115+
if os.path.exists(path):
116+
if not os.path.splitext(path)[1] == ".toml":
117+
raise RuntimeError(f"Analysis file {path} must end in .toml.")
118+
119+
with open(path, "rb") as f:
120+
analysis_toml = util._load_toml(f, "analysis")
121+
122+
if "codebase" in analysis_toml:
123+
if "exclude" in analysis_toml["codebase"]:
124+
args.excludes += analysis_toml["codebase"]["exclude"]
125+
126+
for name in args.platforms:
127+
if name not in analysis_toml["platform"].keys():
128+
raise KeyError(
129+
f"Platform {name} requested on the command line "
130+
+ "does not exist in the configuration file.",
131+
)
132+
133+
cmd_platforms = args.platforms.copy()
134+
for name in analysis_toml["platform"].keys():
135+
if cmd_platforms and name not in cmd_platforms:
136+
continue
137+
if "commands" not in analysis_toml["platform"][name]:
138+
raise ValueError(f"Missing 'commands' for platform {name}")
139+
p = analysis_toml["platform"][name]["commands"]
140+
db = config.load_database(p, rootdir)
141+
args.platforms.append(name)
142+
configuration.update({name: db})
143+
144+
# Construct a codebase object associated with the root directory.
145+
codebase = CodeBase(rootdir, exclude_patterns=args.excludes)
146+
147+
# Parse the source tree, and determine source line associations.
148+
# The trees and associations are housed in state.
149+
state = finder.find(
150+
rootdir,
151+
codebase,
152+
configuration,
153+
show_progress=True,
154+
)
155+
156+
# Print the file tree.
157+
report.files(codebase, state, prune=args.prune, levels=args.levels)
158+
sys.exit(0)
159+
160+
161+
def cli(argv: list[str]) -> int:
162+
parser = _build_parser()
163+
args = parser.parse_args(argv)
164+
165+
# Configure logging such that:
166+
# - Only errors are written to the terminal
167+
log.setLevel(logging.DEBUG)
168+
169+
stderr_handler = logging.StreamHandler(sys.stderr)
170+
stderr_handler.setLevel(logging.ERROR)
171+
stderr_handler.setFormatter(Formatter(colors=sys.stderr.isatty()))
172+
log.addHandler(stderr_handler)
173+
174+
return _tree(args)
175+
176+
177+
def main():
178+
try:
179+
cli(sys.argv[1:])
180+
except Exception as e:
181+
log.error(str(e))
182+
sys.exit(1)
183+
184+
185+
if __name__ == "__main__":
186+
sys.argv[0] = "codebasin.tree"
187+
main()

pyproject.toml

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ dependencies = [
3838
[project.scripts]
3939
codebasin = "codebasin:__main__.main"
4040
cbi-cov = "codebasin.coverage:__main__.main"
41+
cbi-tree = "codebasin:tree.main"
4142

4243
[project.urls]
4344
"Github" = "https://www.github.com/intel/code-base-investigator"

0 commit comments

Comments
 (0)