codebase-stats 0.0.1__tar.gz → 0.0.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {codebase_stats-0.0.1 → codebase_stats-0.0.2}/PKG-INFO +1 -1
- {codebase_stats-0.0.1 → codebase_stats-0.0.2}/codebase_stats/__init__.py +6 -2
- codebase_stats-0.0.2/codebase_stats/cli.py +381 -0
- {codebase_stats-0.0.1 → codebase_stats-0.0.2}/codebase_stats/core.py +19 -19
- {codebase_stats-0.0.1 → codebase_stats-0.0.2}/codebase_stats/coverage.py +15 -9
- {codebase_stats-0.0.1 → codebase_stats-0.0.2}/codebase_stats/duration.py +17 -10
- {codebase_stats-0.0.1 → codebase_stats-0.0.2}/codebase_stats/lowcov.py +9 -8
- {codebase_stats-0.0.1 → codebase_stats-0.0.2}/codebase_stats/metrics.py +19 -16
- {codebase_stats-0.0.1 → codebase_stats-0.0.2}/codebase_stats/radon.py +22 -6
- {codebase_stats-0.0.1 → codebase_stats-0.0.2}/codebase_stats/reporter.py +1 -1
- {codebase_stats-0.0.1 → codebase_stats-0.0.2}/codebase_stats/sizes.py +7 -3
- {codebase_stats-0.0.1 → codebase_stats-0.0.2}/codebase_stats/tree.py +8 -4
- {codebase_stats-0.0.1 → codebase_stats-0.0.2}/codebase_stats/utils.py +5 -6
- {codebase_stats-0.0.1 → codebase_stats-0.0.2}/codebase_stats.egg-info/PKG-INFO +1 -1
- {codebase_stats-0.0.1 → codebase_stats-0.0.2}/codebase_stats.egg-info/SOURCES.txt +1 -0
- codebase_stats-0.0.2/codebase_stats.egg-info/entry_points.txt +2 -0
- {codebase_stats-0.0.1 → codebase_stats-0.0.2}/pyproject.toml +2 -3
- codebase_stats-0.0.1/codebase_stats.egg-info/entry_points.txt +0 -2
- {codebase_stats-0.0.1 → codebase_stats-0.0.2}/README.md +0 -0
- {codebase_stats-0.0.1 → codebase_stats-0.0.2}/codebase_stats.egg-info/dependency_links.txt +0 -0
- {codebase_stats-0.0.1 → codebase_stats-0.0.2}/codebase_stats.egg-info/requires.txt +0 -0
- {codebase_stats-0.0.1 → codebase_stats-0.0.2}/codebase_stats.egg-info/top_level.txt +0 -0
- {codebase_stats-0.0.1 → codebase_stats-0.0.2}/setup.cfg +0 -0
- {codebase_stats-0.0.1 → codebase_stats-0.0.2}/tests/test_core.py +0 -0
- {codebase_stats-0.0.1 → codebase_stats-0.0.2}/tests/test_coverage.py +0 -0
- {codebase_stats-0.0.1 → codebase_stats-0.0.2}/tests/test_coverage_gaps.py +0 -0
- {codebase_stats-0.0.1 → codebase_stats-0.0.2}/tests/test_duration.py +0 -0
- {codebase_stats-0.0.1 → codebase_stats-0.0.2}/tests/test_integration.py +0 -0
- {codebase_stats-0.0.1 → codebase_stats-0.0.2}/tests/test_lowcov.py +0 -0
- {codebase_stats-0.0.1 → codebase_stats-0.0.2}/tests/test_metrics.py +0 -0
- {codebase_stats-0.0.1 → codebase_stats-0.0.2}/tests/test_metrics_mocked.py +0 -0
- {codebase_stats-0.0.1 → codebase_stats-0.0.2}/tests/test_sizes.py +0 -0
- {codebase_stats-0.0.1 → codebase_stats-0.0.2}/tests/test_tree.py +0 -0
- {codebase_stats-0.0.1 → codebase_stats-0.0.2}/tests/test_utils.py +0 -0
|
@@ -45,12 +45,16 @@ from .coverage import (
|
|
|
45
45
|
show_pragma_histogram,
|
|
46
46
|
)
|
|
47
47
|
|
|
48
|
-
#
|
|
49
|
-
from .
|
|
48
|
+
# Radon analysis
|
|
49
|
+
from .radon import (
|
|
50
50
|
run_radon,
|
|
51
51
|
run_radon_mi,
|
|
52
52
|
run_radon_raw,
|
|
53
53
|
run_radon_hal,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Code quality metrics
|
|
57
|
+
from .metrics import (
|
|
54
58
|
show_complexity_histogram,
|
|
55
59
|
show_mi_histogram,
|
|
56
60
|
show_raw_histogram,
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
coverage_report.py — Coverage inspector + test report analyser.
|
|
4
|
+
|
|
5
|
+
Features:
|
|
6
|
+
1. Coverage distribution histogram (needs coverage.json)
|
|
7
|
+
2. Low-coverage file listing (needs coverage.json)
|
|
8
|
+
3. Test duration histogram (needs --report report.json)
|
|
9
|
+
4. File size distribution histogram (scanned from disk)
|
|
10
|
+
|
|
11
|
+
Generate the inputs with:
|
|
12
|
+
pip install pytest-json-report
|
|
13
|
+
pytest ... --cov=app --cov-report=json \
|
|
14
|
+
--json-report --json-report-file=report.json
|
|
15
|
+
|
|
16
|
+
--show options (combinable, default: all available):
|
|
17
|
+
coverage coverage distribution histogram + blame
|
|
18
|
+
duration test duration histogram + blame (requires --report)
|
|
19
|
+
sizes file size histogram + blame (requires --fs-root)
|
|
20
|
+
list low-coverage file table
|
|
21
|
+
histograms shorthand for all histograms: coverage duration sizes complexity mi raw hal (no blame, no table)
|
|
22
|
+
complexity cyclomatic complexity histogram + high-CC/low-coverage blame (requires --radon-root)
|
|
23
|
+
mi maintainability index histogram + low-MI blame (requires --radon-root)
|
|
24
|
+
raw comment ratio histogram + under-documented blame (requires --radon-root)
|
|
25
|
+
hal halstead metrics histogram + high-bug-estimate blame (requires --radon-root)
|
|
26
|
+
tree file tree analyzer (counts files and subfolders) (requires --tree-root)
|
|
27
|
+
blame quality blame sections only (combine with histograms or coverage/duration/sizes)
|
|
28
|
+
all everything
|
|
29
|
+
|
|
30
|
+
Usage:
|
|
31
|
+
python cli.py coverage.json
|
|
32
|
+
python cli.py coverage.json --report report.json
|
|
33
|
+
python cli.py coverage.json --show histograms --report report.json --fs-root app
|
|
34
|
+
python cli.py coverage.json --show coverage
|
|
35
|
+
python cli.py coverage.json --report report.json --show duration
|
|
36
|
+
python cli.py coverage.json --show list --threshold 80
|
|
37
|
+
python cli.py coverage.json --report report.json --show coverage duration
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
import argparse
|
|
41
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
42
|
+
|
|
43
|
+
from codebase_stats import (
|
|
44
|
+
analyze_tree,
|
|
45
|
+
load_coverage,
|
|
46
|
+
load_report,
|
|
47
|
+
parse_sorts,
|
|
48
|
+
precompute_coverage_stats,
|
|
49
|
+
run_radon,
|
|
50
|
+
run_radon_hal,
|
|
51
|
+
run_radon_mi,
|
|
52
|
+
run_radon_raw,
|
|
53
|
+
scan_pragma_counts,
|
|
54
|
+
show_complexity_histogram,
|
|
55
|
+
show_coverage_histogram,
|
|
56
|
+
show_duration_histogram,
|
|
57
|
+
show_file_size_distribution,
|
|
58
|
+
show_hal_histogram,
|
|
59
|
+
show_low_coverage,
|
|
60
|
+
show_mi_histogram,
|
|
61
|
+
show_raw_histogram,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
VALID_VIEWS = {
|
|
65
|
+
"coverage",
|
|
66
|
+
"duration",
|
|
67
|
+
"sizes",
|
|
68
|
+
"list",
|
|
69
|
+
"histograms",
|
|
70
|
+
"complexity",
|
|
71
|
+
"mi",
|
|
72
|
+
"raw",
|
|
73
|
+
"hal",
|
|
74
|
+
"tree",
|
|
75
|
+
"blame",
|
|
76
|
+
"all",
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def main():
|
|
81
|
+
p = argparse.ArgumentParser(
|
|
82
|
+
description="Coverage inspector + test report analyser",
|
|
83
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
84
|
+
epilog=__doc__,
|
|
85
|
+
)
|
|
86
|
+
p.add_argument(
|
|
87
|
+
"coverage_file",
|
|
88
|
+
nargs="?",
|
|
89
|
+
default="coverage.json",
|
|
90
|
+
help="Path to coverage.json (default: coverage.json)",
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Duration
|
|
94
|
+
p.add_argument(
|
|
95
|
+
"--report",
|
|
96
|
+
metavar="FILE",
|
|
97
|
+
default=None,
|
|
98
|
+
help="pytest-json-report file for the duration histogram",
|
|
99
|
+
)
|
|
100
|
+
p.add_argument(
|
|
101
|
+
"--slow-threshold",
|
|
102
|
+
type=float,
|
|
103
|
+
default=1.0,
|
|
104
|
+
metavar="SECS",
|
|
105
|
+
help="Slow-test threshold in seconds (default: 1.0)",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# File sizes
|
|
109
|
+
p.add_argument(
|
|
110
|
+
"--fs-root",
|
|
111
|
+
default=".",
|
|
112
|
+
metavar="DIR",
|
|
113
|
+
help="Root directory to scan for file-size histogram (default: .)",
|
|
114
|
+
)
|
|
115
|
+
p.add_argument(
|
|
116
|
+
"--ext", default="py", metavar="EXT", help="File extension to measure (default: py)"
|
|
117
|
+
)
|
|
118
|
+
p.add_argument(
|
|
119
|
+
"--fs-percentiles",
|
|
120
|
+
default="25,50,75,90,95,99",
|
|
121
|
+
metavar="LIST",
|
|
122
|
+
help="Comma-separated percentiles for file-size histogram",
|
|
123
|
+
)
|
|
124
|
+
p.add_argument(
|
|
125
|
+
"--fs-above",
|
|
126
|
+
type=int,
|
|
127
|
+
default=None,
|
|
128
|
+
metavar="PCT",
|
|
129
|
+
help="List files above this percentile (e.g. 95)",
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# File tree
|
|
133
|
+
p.add_argument(
|
|
134
|
+
"--tree-root",
|
|
135
|
+
default=None,
|
|
136
|
+
metavar="DIR",
|
|
137
|
+
help="Root directory for file tree analysis (e.g. app)",
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Radon analysis root (used by --show complexity / mi / raw / hal)
|
|
141
|
+
p.add_argument(
|
|
142
|
+
"--radon-root",
|
|
143
|
+
default=None,
|
|
144
|
+
metavar="DIR",
|
|
145
|
+
help="Root directory for all radon analyses (e.g. app)",
|
|
146
|
+
)
|
|
147
|
+
p.add_argument(
|
|
148
|
+
"--debug-radon",
|
|
149
|
+
action="store_true",
|
|
150
|
+
help="Print first 2 raw entries from each radon command for path diagnostics",
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Low-coverage listing
|
|
154
|
+
p.add_argument(
|
|
155
|
+
"--threshold", type=float, default=90.0, help="Show files below this %% (default: 50)"
|
|
156
|
+
)
|
|
157
|
+
p.add_argument(
|
|
158
|
+
"--max",
|
|
159
|
+
dest="max_threshold",
|
|
160
|
+
type=float,
|
|
161
|
+
default=None,
|
|
162
|
+
help="Upper bound %% (range filter)",
|
|
163
|
+
)
|
|
164
|
+
p.add_argument(
|
|
165
|
+
"--top",
|
|
166
|
+
dest="top_n",
|
|
167
|
+
type=int,
|
|
168
|
+
default=20,
|
|
169
|
+
help="Max rows in file listing (0 = all, default: 20)",
|
|
170
|
+
)
|
|
171
|
+
p.add_argument(
|
|
172
|
+
"--sort",
|
|
173
|
+
dest="sort_specs",
|
|
174
|
+
nargs="+",
|
|
175
|
+
default=["priority:desc"],
|
|
176
|
+
metavar="FIELD[:asc|desc]",
|
|
177
|
+
help="Sort fields: priority coverage layer missing missing_pct",
|
|
178
|
+
)
|
|
179
|
+
p.add_argument("--sort-order", choices=["asc", "desc"], default="desc")
|
|
180
|
+
p.add_argument(
|
|
181
|
+
"--show-lines", action="store_true", help="Print missing line numbers under each file"
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# Shared
|
|
185
|
+
p.add_argument("--bins", type=int, default=10, help="Histogram bin count (default: 10)")
|
|
186
|
+
p.add_argument(
|
|
187
|
+
"--blame-limit",
|
|
188
|
+
type=int,
|
|
189
|
+
default=20,
|
|
190
|
+
metavar="N",
|
|
191
|
+
help="Max entries in quality blame sections (default: 20, 0 = all)",
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# View selector
|
|
195
|
+
p.add_argument(
|
|
196
|
+
"--show",
|
|
197
|
+
nargs="+",
|
|
198
|
+
metavar="VIEW",
|
|
199
|
+
default=["all"],
|
|
200
|
+
help=(
|
|
201
|
+
"Sections to display. Options: "
|
|
202
|
+
"coverage duration sizes list histograms blame all. "
|
|
203
|
+
"Default: all. histograms = bars only (no blame, no table). complexity = CC histogram. "
|
|
204
|
+
"Add blame to include quality blame sections."
|
|
205
|
+
),
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
args = p.parse_args()
|
|
209
|
+
|
|
210
|
+
# Validate views
|
|
211
|
+
unknown = set(args.show) - VALID_VIEWS
|
|
212
|
+
if unknown:
|
|
213
|
+
p.error(
|
|
214
|
+
f"Unknown --show value(s): {', '.join(sorted(unknown))}. "
|
|
215
|
+
f"Valid: {', '.join(sorted(VALID_VIEWS))}"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
views = set(args.show)
|
|
219
|
+
show_all = "all" in views
|
|
220
|
+
show_hists = "histograms" in views # shorthand: coverage+duration+sizes (no blame, no list)
|
|
221
|
+
want_cov = show_all or show_hists or "coverage" in views
|
|
222
|
+
want_dur = show_all or show_hists or "duration" in views
|
|
223
|
+
want_size = show_all or show_hists or "sizes" in views
|
|
224
|
+
want_blame = show_all or "blame" in views # histograms alone → no blame
|
|
225
|
+
want_complexity = show_all or show_hists or "complexity" in views
|
|
226
|
+
want_mi = show_all or show_hists or "mi" in views
|
|
227
|
+
want_raw = show_all or show_hists or "raw" in views
|
|
228
|
+
want_hal = show_all or show_hists or "hal" in views
|
|
229
|
+
want_tree = show_all or "tree" in views
|
|
230
|
+
want_list = show_all or "list" in views
|
|
231
|
+
|
|
232
|
+
if want_dur and not args.report:
|
|
233
|
+
if "duration" in views or show_hists:
|
|
234
|
+
p.error("--show duration/histograms requires --report <report.json>")
|
|
235
|
+
want_dur = False # silently skip when --show all and no report provided
|
|
236
|
+
|
|
237
|
+
pcts = [int(x.strip()) for x in args.fs_percentiles.split(",") if x.strip()]
|
|
238
|
+
top_n = args.top_n if args.top_n > 0 else None
|
|
239
|
+
sorts = parse_sorts(args.sort_specs, args.sort_order)
|
|
240
|
+
|
|
241
|
+
# Optionally run radon analyses
|
|
242
|
+
radon_root = args.radon_root
|
|
243
|
+
any_radon = want_complexity or want_mi or want_raw or want_hal
|
|
244
|
+
if any_radon and not radon_root:
|
|
245
|
+
needs = [
|
|
246
|
+
v
|
|
247
|
+
for v, w in [
|
|
248
|
+
("complexity", want_complexity),
|
|
249
|
+
("mi", want_mi),
|
|
250
|
+
("raw", want_raw),
|
|
251
|
+
("hal", want_hal),
|
|
252
|
+
]
|
|
253
|
+
if w
|
|
254
|
+
]
|
|
255
|
+
print(f"⚠️ --show {'/'.join(needs)} requires --radon-root <dir> (skipping)")
|
|
256
|
+
want_complexity = want_mi = want_raw = want_hal = False
|
|
257
|
+
|
|
258
|
+
_dbg = args.debug_radon
|
|
259
|
+
|
|
260
|
+
# Run all requested radon subcommands in parallel
|
|
261
|
+
if radon_root:
|
|
262
|
+
tasks = {}
|
|
263
|
+
wanted = {
|
|
264
|
+
"cc": want_complexity,
|
|
265
|
+
"mi": want_mi,
|
|
266
|
+
"raw": want_raw,
|
|
267
|
+
"hal": want_hal,
|
|
268
|
+
}
|
|
269
|
+
runners = {
|
|
270
|
+
"cc": lambda: run_radon(radon_root, debug=_dbg),
|
|
271
|
+
"mi": lambda: run_radon_mi(radon_root, debug=_dbg),
|
|
272
|
+
"raw": lambda: run_radon_raw(radon_root, debug=_dbg),
|
|
273
|
+
"hal": lambda: run_radon_hal(radon_root, debug=_dbg),
|
|
274
|
+
}
|
|
275
|
+
# Always run all four when radon-root is given so the summary has full
|
|
276
|
+
# data — but only the explicitly-requested ones block the display.
|
|
277
|
+
with ThreadPoolExecutor(max_workers=4) as pool:
|
|
278
|
+
futures = {pool.submit(fn): key for key, fn in runners.items()}
|
|
279
|
+
results = {key: {} for key in runners}
|
|
280
|
+
for future in as_completed(futures):
|
|
281
|
+
key = futures[future]
|
|
282
|
+
try:
|
|
283
|
+
results[key] = future.result()
|
|
284
|
+
except Exception as e:
|
|
285
|
+
print(f"⚠️ radon {key} failed: {e}")
|
|
286
|
+
complexity_map, mi_map, raw_map, hal_map = (
|
|
287
|
+
results["cc"],
|
|
288
|
+
results["mi"],
|
|
289
|
+
results["raw"],
|
|
290
|
+
results["hal"],
|
|
291
|
+
)
|
|
292
|
+
if _dbg:
|
|
293
|
+
for name, m in [
|
|
294
|
+
("cc", complexity_map),
|
|
295
|
+
("mi", mi_map),
|
|
296
|
+
("raw", raw_map),
|
|
297
|
+
("hal", hal_map),
|
|
298
|
+
]:
|
|
299
|
+
print(f" [debug] {name}: {len(m)} entries sample key: {next(iter(m), 'n/a')!r}")
|
|
300
|
+
else:
|
|
301
|
+
complexity_map = mi_map = raw_map = hal_map = {}
|
|
302
|
+
|
|
303
|
+
# Pragma: no cover scan (use radon_root if available, else fs_root)
|
|
304
|
+
pragma_root = radon_root if radon_root else (args.fs_root if args.fs_root != "." else None)
|
|
305
|
+
pragma_counts: dict[str, int] = {}
|
|
306
|
+
if want_cov and pragma_root:
|
|
307
|
+
pragma_counts = scan_pragma_counts(pragma_root)
|
|
308
|
+
|
|
309
|
+
# Single precompute pass over coverage.json
|
|
310
|
+
cov_data = load_coverage(args.coverage_file)
|
|
311
|
+
stats = precompute_coverage_stats(cov_data, complexity_map, mi_map, raw_map, hal_map)
|
|
312
|
+
|
|
313
|
+
if want_cov:
|
|
314
|
+
show_coverage_histogram(
|
|
315
|
+
stats,
|
|
316
|
+
bins=args.bins,
|
|
317
|
+
blame_limit=args.blame_limit,
|
|
318
|
+
show_blame=want_blame,
|
|
319
|
+
threshold=args.threshold,
|
|
320
|
+
pragma_counts=pragma_counts,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
if want_dur:
|
|
324
|
+
show_duration_histogram(
|
|
325
|
+
load_report(args.report),
|
|
326
|
+
bins=args.bins,
|
|
327
|
+
slow_threshold=args.slow_threshold,
|
|
328
|
+
blame_limit=args.blame_limit,
|
|
329
|
+
show_blame=want_blame,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
if want_size:
|
|
333
|
+
show_file_size_distribution(
|
|
334
|
+
root=args.fs_root,
|
|
335
|
+
extension=args.ext,
|
|
336
|
+
percentiles=pcts,
|
|
337
|
+
show_above_pct=args.fs_above,
|
|
338
|
+
bins=args.bins,
|
|
339
|
+
blame_limit=args.blame_limit,
|
|
340
|
+
show_blame=want_blame,
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
if want_list:
|
|
344
|
+
show_low_coverage(
|
|
345
|
+
stats,
|
|
346
|
+
threshold=args.threshold,
|
|
347
|
+
max_threshold=args.max_threshold,
|
|
348
|
+
top_n=top_n,
|
|
349
|
+
sorts=sorts,
|
|
350
|
+
show_lines=args.show_lines,
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
if want_complexity:
|
|
354
|
+
show_complexity_histogram(
|
|
355
|
+
stats, bins=args.bins, blame_limit=args.blame_limit, show_blame=want_blame
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
if want_mi:
|
|
359
|
+
show_mi_histogram(
|
|
360
|
+
stats, bins=args.bins, blame_limit=args.blame_limit, show_blame=want_blame
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
if want_raw:
|
|
364
|
+
show_raw_histogram(
|
|
365
|
+
stats, bins=args.bins, blame_limit=args.blame_limit, show_blame=want_blame
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
if want_hal:
|
|
369
|
+
show_hal_histogram(
|
|
370
|
+
stats, bins=args.bins, blame_limit=args.blame_limit, show_blame=want_blame
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
if want_tree:
|
|
374
|
+
if args.tree_root:
|
|
375
|
+
analyze_tree(args.tree_root, bins=args.bins, blame_limit=args.blame_limit)
|
|
376
|
+
else:
|
|
377
|
+
print("⚠️ --show tree requires --tree-root <dir>")
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
if __name__ == "__main__":
|
|
381
|
+
main()
|
|
@@ -7,13 +7,13 @@ from pathlib import Path
|
|
|
7
7
|
|
|
8
8
|
def load_coverage(path: str) -> dict:
|
|
9
9
|
"""Load and parse a coverage.json file.
|
|
10
|
-
|
|
10
|
+
|
|
11
11
|
Args:
|
|
12
12
|
path: Path to coverage.json file
|
|
13
|
-
|
|
13
|
+
|
|
14
14
|
Returns:
|
|
15
15
|
Parsed coverage data dictionary
|
|
16
|
-
|
|
16
|
+
|
|
17
17
|
Raises:
|
|
18
18
|
SystemExit: On file not found or invalid JSON
|
|
19
19
|
"""
|
|
@@ -31,13 +31,13 @@ def load_coverage(path: str) -> dict:
|
|
|
31
31
|
|
|
32
32
|
def load_report(path: str) -> dict:
|
|
33
33
|
"""Load and parse a pytest-json-report file.
|
|
34
|
-
|
|
34
|
+
|
|
35
35
|
Args:
|
|
36
36
|
path: Path to pytest JSON report file
|
|
37
|
-
|
|
37
|
+
|
|
38
38
|
Returns:
|
|
39
39
|
Parsed report data dictionary
|
|
40
|
-
|
|
40
|
+
|
|
41
41
|
Raises:
|
|
42
42
|
SystemExit: On file not found or invalid JSON
|
|
43
43
|
"""
|
|
@@ -78,10 +78,10 @@ LAYER_ORDER = {
|
|
|
78
78
|
|
|
79
79
|
def extract_layer(path: str) -> str:
|
|
80
80
|
"""Extract the architectural layer from a file path.
|
|
81
|
-
|
|
81
|
+
|
|
82
82
|
Args:
|
|
83
83
|
path: File path to analyze
|
|
84
|
-
|
|
84
|
+
|
|
85
85
|
Returns:
|
|
86
86
|
Layer name (e.g., "Domain", "Application", "Other")
|
|
87
87
|
"""
|
|
@@ -94,19 +94,19 @@ def extract_layer(path: str) -> str:
|
|
|
94
94
|
|
|
95
95
|
def build_suffix_index(radon_map: dict) -> dict:
|
|
96
96
|
"""Build a suffix-keyed index for path matching.
|
|
97
|
-
|
|
98
|
-
This allows matching coverage.json paths (which may use different root
|
|
97
|
+
|
|
98
|
+
This allows matching coverage.json paths (which may use different root
|
|
99
99
|
prefixes or be absolute vs relative) to radon output.
|
|
100
|
-
|
|
100
|
+
|
|
101
101
|
Strategy: for each radon key, store it under every possible suffix:
|
|
102
102
|
'/home/user/proj/app/foo/bar.py' → keys: 'bar.py', 'foo/bar.py',
|
|
103
103
|
'app/foo/bar.py', ...
|
|
104
104
|
Then look up a coverage path by trying progressively longer suffixes
|
|
105
105
|
until one hits. The longest match wins to avoid false collisions.
|
|
106
|
-
|
|
106
|
+
|
|
107
107
|
Args:
|
|
108
108
|
radon_map: Dictionary mapping file paths to radon analysis data
|
|
109
|
-
|
|
109
|
+
|
|
110
110
|
Returns:
|
|
111
111
|
Dictionary mapping suffixes to radon values
|
|
112
112
|
"""
|
|
@@ -123,13 +123,13 @@ def build_suffix_index(radon_map: dict) -> dict:
|
|
|
123
123
|
|
|
124
124
|
def suffix_lookup(index: dict, coverage_path: str):
|
|
125
125
|
"""Look up a coverage.json path in a suffix index.
|
|
126
|
-
|
|
126
|
+
|
|
127
127
|
Try suffixes from most-specific (full path) down to basename.
|
|
128
|
-
|
|
128
|
+
|
|
129
129
|
Args:
|
|
130
130
|
index: Index built by build_suffix_index()
|
|
131
131
|
coverage_path: Path from coverage.json to look up
|
|
132
|
-
|
|
132
|
+
|
|
133
133
|
Returns:
|
|
134
134
|
The radon value if found, None otherwise
|
|
135
135
|
"""
|
|
@@ -149,17 +149,17 @@ def precompute_coverage_stats(
|
|
|
149
149
|
hal_map: dict = None,
|
|
150
150
|
) -> dict:
|
|
151
151
|
"""Precompute statistics from coverage.json and radon data.
|
|
152
|
-
|
|
152
|
+
|
|
153
153
|
One pass over data['files']. Returns everything both histogram and
|
|
154
154
|
list functions need so neither has to re-iterate the JSON.
|
|
155
|
-
|
|
155
|
+
|
|
156
156
|
Args:
|
|
157
157
|
data: Parsed coverage.json data
|
|
158
158
|
complexity_map: Radon cyclomatic complexity data (optional)
|
|
159
159
|
mi_map: Radon maintainability index data (optional)
|
|
160
160
|
raw_map: Radon raw metrics data (optional)
|
|
161
161
|
hal_map: Radon Halstead metrics data (optional)
|
|
162
|
-
|
|
162
|
+
|
|
163
163
|
Returns:
|
|
164
164
|
Dictionary with precomputed statistics including file_stats, project totals,
|
|
165
165
|
and coverage percentiles
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
"""Coverage analysis functions."""
|
|
2
2
|
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
from .utils import ascii_histogram, blame_header, format_line_ranges, percentile
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
def scan_pragma_intervals(root: str) -> dict[str, list]:
|
|
8
9
|
"""Find sequential # pragma: no cover blocks per .py file under root.
|
|
9
|
-
|
|
10
|
+
|
|
10
11
|
Args:
|
|
11
12
|
root: Root directory to scan
|
|
12
|
-
|
|
13
|
+
|
|
13
14
|
Returns:
|
|
14
15
|
Dictionary mapping file paths to lists of (start, end) tuples
|
|
15
16
|
"""
|
|
@@ -40,10 +41,10 @@ def scan_pragma_intervals(root: str) -> dict[str, list]:
|
|
|
40
41
|
|
|
41
42
|
def scan_pragma_counts(root: str) -> dict[str, int]:
|
|
42
43
|
"""Count '# pragma: no cover' occurrences per .py file under root.
|
|
43
|
-
|
|
44
|
+
|
|
44
45
|
Args:
|
|
45
46
|
root: Root directory to scan
|
|
46
|
-
|
|
47
|
+
|
|
47
48
|
Returns:
|
|
48
49
|
Dictionary mapping file paths to pragma counts
|
|
49
50
|
"""
|
|
@@ -60,12 +61,14 @@ def scan_pragma_counts(root: str) -> dict[str, int]:
|
|
|
60
61
|
return result
|
|
61
62
|
|
|
62
63
|
|
|
63
|
-
def show_pragma_histogram(
|
|
64
|
+
def show_pragma_histogram(
|
|
65
|
+
pragma_counts: dict, bins: int = 10, blame_limit: int = 20, width: int = 80
|
|
66
|
+
) -> None:
|
|
64
67
|
"""Histogram of '# pragma: no cover' count per file + all-files listing.
|
|
65
68
|
|
|
66
69
|
Also prints a secondary histogram and table for sequential blocks of
|
|
67
70
|
pragmas (intervals) so that you can spot long runs of skipped code.
|
|
68
|
-
|
|
71
|
+
|
|
69
72
|
Args:
|
|
70
73
|
pragma_counts: Dictionary mapping files to pragma counts
|
|
71
74
|
bins: Number of histogram bins
|
|
@@ -173,7 +176,7 @@ def show_coverage_histogram(
|
|
|
173
176
|
width: int = 80,
|
|
174
177
|
):
|
|
175
178
|
"""Display coverage distribution histogram and low-coverage blame.
|
|
176
|
-
|
|
179
|
+
|
|
177
180
|
Args:
|
|
178
181
|
stats: Precomputed statistics from precompute_coverage_stats()
|
|
179
182
|
bins: Number of histogram bins
|
|
@@ -228,6 +231,7 @@ def show_coverage_histogram(
|
|
|
228
231
|
missing_counts = sorted(f["missing_count"] for f in stats["file_stats"] if f["pct"] < 100.0)
|
|
229
232
|
if missing_counts:
|
|
230
233
|
import math
|
|
234
|
+
|
|
231
235
|
nm = len(missing_counts)
|
|
232
236
|
total_missing = sum(missing_counts)
|
|
233
237
|
m_max = missing_counts[-1]
|
|
@@ -282,7 +286,9 @@ def show_coverage_histogram(
|
|
|
282
286
|
],
|
|
283
287
|
key=lambda x: x[0],
|
|
284
288
|
)
|
|
285
|
-
blame_header(
|
|
289
|
+
blame_header(
|
|
290
|
+
f"below {blame_ceiling:.1f}% ({boundary_note})", len(blamed), blame_limit, width
|
|
291
|
+
)
|
|
286
292
|
display = blamed if not blame_limit else blamed[:blame_limit]
|
|
287
293
|
if display:
|
|
288
294
|
for pct, path, missing_count, missing_lines in display:
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
"""Test duration and performance analysis."""
|
|
2
2
|
|
|
3
3
|
import math
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
from .utils import ascii_histogram, blame_header, fmt_seconds, percentile
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
def test_duration(t: dict):
|
|
8
9
|
"""Get (total_duration, breakdown_dict) for a test.
|
|
9
|
-
|
|
10
|
+
|
|
10
11
|
Sum setup, call and teardown phases.
|
|
11
|
-
|
|
12
|
+
|
|
12
13
|
Args:
|
|
13
14
|
t: Test dictionary from pytest-json-report
|
|
14
|
-
|
|
15
|
+
|
|
15
16
|
Returns:
|
|
16
17
|
Tuple of (total_duration_seconds, phase_breakdown_dict)
|
|
17
18
|
"""
|
|
@@ -32,7 +33,7 @@ def test_duration(t: dict):
|
|
|
32
33
|
|
|
33
34
|
def render_duration_stats(ds: list, slow_threshold: float, width: int = 80):
|
|
34
35
|
"""Render summary statistics for a duration list.
|
|
35
|
-
|
|
36
|
+
|
|
36
37
|
Args:
|
|
37
38
|
ds: Sorted list of durations
|
|
38
39
|
slow_threshold: Threshold for considering tests slow
|
|
@@ -67,7 +68,7 @@ def render_duration_histogram_core(
|
|
|
67
68
|
width: int = 80,
|
|
68
69
|
):
|
|
69
70
|
"""Render the ASCII histogram for a duration list.
|
|
70
|
-
|
|
71
|
+
|
|
71
72
|
Args:
|
|
72
73
|
ds: Sorted list of durations
|
|
73
74
|
title: Title for the histogram
|
|
@@ -138,7 +139,7 @@ def show_duration_histogram(
|
|
|
138
139
|
width: int = 80,
|
|
139
140
|
):
|
|
140
141
|
"""Display test duration histogram and slow test blame.
|
|
141
|
-
|
|
142
|
+
|
|
142
143
|
Args:
|
|
143
144
|
report: Parsed pytest-json-report data
|
|
144
145
|
bins: Number of histogram bins
|
|
@@ -181,7 +182,9 @@ def show_duration_histogram(
|
|
|
181
182
|
report_total = report.get("duration", 0)
|
|
182
183
|
collection_time = report_total - total if report_total > total else 0
|
|
183
184
|
|
|
184
|
-
print(
|
|
185
|
+
print(
|
|
186
|
+
f" Tests: {n} Total: {fmt_seconds(total)} ({fmt_seconds(report_total)} wall time) {outcome_str}"
|
|
187
|
+
)
|
|
185
188
|
if collection_time > 1:
|
|
186
189
|
print(f" (includes ~{fmt_seconds(collection_time)} collection/overhead)")
|
|
187
190
|
|
|
@@ -194,7 +197,9 @@ def show_duration_histogram(
|
|
|
194
197
|
|
|
195
198
|
# 2. Show call histogram
|
|
196
199
|
if phases["call"]:
|
|
197
|
-
render_duration_histogram_core(
|
|
200
|
+
render_duration_histogram_core(
|
|
201
|
+
sorted(phases["call"]), "PHASE: CALL", bins, slow_threshold, width
|
|
202
|
+
)
|
|
198
203
|
print(f"\n {'-' * (width - 4)}")
|
|
199
204
|
|
|
200
205
|
# 3. Show teardown histogram
|
|
@@ -205,7 +210,9 @@ def show_duration_histogram(
|
|
|
205
210
|
print(f"\n {'-' * (width - 4)}")
|
|
206
211
|
|
|
207
212
|
# 4. Show total duration histogram
|
|
208
|
-
render_duration_histogram_core(
|
|
213
|
+
render_duration_histogram_core(
|
|
214
|
+
durations, "AGGREGATE: TOTAL DURATION", bins, slow_threshold, width
|
|
215
|
+
)
|
|
209
216
|
|
|
210
217
|
if show_blame:
|
|
211
218
|
q1_dur = percentile(durations, 25)
|
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
"""Low-coverage file listing and prioritization."""
|
|
2
2
|
|
|
3
|
-
from .utils import format_line_ranges
|
|
4
3
|
from .metrics import cc_rank, mi_rank
|
|
5
|
-
|
|
4
|
+
from .utils import format_line_ranges
|
|
6
5
|
|
|
7
6
|
VALID_SORT_FIELDS = {"priority", "coverage", "layer", "missing", "missing_pct", "complexity"}
|
|
8
7
|
|
|
9
8
|
|
|
10
9
|
def parse_sorts(sort_specs: list, default_order: str) -> list:
|
|
11
10
|
"""Parse sort specifications into (field, is_descending) tuples.
|
|
12
|
-
|
|
11
|
+
|
|
13
12
|
Args:
|
|
14
13
|
sort_specs: List of sort specs like ["priority:desc", "coverage:asc"]
|
|
15
14
|
default_order: Default sort order ("asc" or "desc")
|
|
16
|
-
|
|
15
|
+
|
|
17
16
|
Returns:
|
|
18
17
|
List of (field, is_descending) tuples
|
|
19
18
|
"""
|
|
@@ -32,16 +31,18 @@ def parse_sorts(sort_specs: list, default_order: str) -> list:
|
|
|
32
31
|
return result
|
|
33
32
|
|
|
34
33
|
|
|
35
|
-
def priority_score(
|
|
34
|
+
def priority_score(
|
|
35
|
+
pct: float, layer: str, missing: int, cc_avg: float = None, mi: float = None
|
|
36
|
+
) -> int:
|
|
36
37
|
"""Calculate priority score for a file based on coverage and metrics.
|
|
37
|
-
|
|
38
|
+
|
|
38
39
|
Args:
|
|
39
40
|
pct: Coverage percentage
|
|
40
41
|
layer: Architectural layer
|
|
41
42
|
missing: Number of missing statements
|
|
42
43
|
cc_avg: Average cyclomatic complexity (optional)
|
|
43
44
|
mi: Maintainability index (optional)
|
|
44
|
-
|
|
45
|
+
|
|
45
46
|
Returns:
|
|
46
47
|
Priority score (higher = more important to fix)
|
|
47
48
|
"""
|
|
@@ -68,7 +69,7 @@ def show_low_coverage(
|
|
|
68
69
|
width: int = 100,
|
|
69
70
|
):
|
|
70
71
|
"""Display files with low coverage and quality metrics.
|
|
71
|
-
|
|
72
|
+
|
|
72
73
|
Args:
|
|
73
74
|
stats: Precomputed statistics from precompute_coverage_stats()
|
|
74
75
|
threshold: Show files below this coverage percentage
|
|
@@ -1,25 +1,16 @@
|
|
|
1
1
|
"""Code quality metrics analysis (complexity, maintainability, etc.)."""
|
|
2
2
|
|
|
3
3
|
import bisect
|
|
4
|
-
|
|
4
|
+
|
|
5
5
|
from .radon import ( # re-export so callers can still import from here
|
|
6
6
|
cc_rank,
|
|
7
7
|
mi_rank,
|
|
8
|
-
run_radon_json,
|
|
9
8
|
run_radon,
|
|
10
9
|
run_radon_mi,
|
|
11
10
|
run_radon_raw,
|
|
12
11
|
run_radon_hal,
|
|
13
|
-
_HAL_VOLUME,
|
|
14
|
-
_HAL_DIFFICULTY,
|
|
15
|
-
_HAL_EFFORT,
|
|
16
|
-
_HAL_BUGS,
|
|
17
|
-
_parse_hal_list,
|
|
18
|
-
_parse_hal_dict,
|
|
19
|
-
_parse_hal_entry,
|
|
20
|
-
_run_hal_chunk,
|
|
21
|
-
_collect_hal_raw,
|
|
22
12
|
)
|
|
13
|
+
from .utils import ascii_histogram, blame_header, format_line_ranges, percentile
|
|
23
14
|
|
|
24
15
|
|
|
25
16
|
def _float_buckets(sorted_values: list, n_bins: int) -> tuple:
|
|
@@ -109,9 +100,7 @@ def _blame_raw(raw_files: list, q1_ratio: float, blame_limit: int, width: int) -
|
|
|
109
100
|
[f for f in raw_files if f[0] < q1_ratio],
|
|
110
101
|
key=lambda x: x[0],
|
|
111
102
|
)
|
|
112
|
-
blame_header(
|
|
113
|
-
f"comment ratio below Q1 ({q1_ratio * 100:.1f}%)", len(blamed), blame_limit, width
|
|
114
|
-
)
|
|
103
|
+
blame_header(f"comment ratio below Q1 ({q1_ratio * 100:.1f}%)", len(blamed), blame_limit, width)
|
|
115
104
|
display = blamed if not blame_limit else blamed[:blame_limit]
|
|
116
105
|
if display:
|
|
117
106
|
print(f" {'Comment%':<10} {'SLOC':<7} {'Coverage':<10} File")
|
|
@@ -289,7 +278,7 @@ def show_raw_histogram(
|
|
|
289
278
|
stats: dict, bins: int = 10, blame_limit: int = 20, show_blame: bool = True, width: int = 80
|
|
290
279
|
):
|
|
291
280
|
"""Display comment ratio histogram and under-documented blame.
|
|
292
|
-
|
|
281
|
+
|
|
293
282
|
Args:
|
|
294
283
|
stats: Precomputed statistics from precompute_coverage_stats()
|
|
295
284
|
bins: Number of histogram bins
|
|
@@ -343,7 +332,7 @@ def show_hal_histogram(
|
|
|
343
332
|
stats: dict, bins: int = 10, blame_limit: int = 20, show_blame: bool = True, width: int = 80
|
|
344
333
|
):
|
|
345
334
|
"""Display Halstead metrics histogram and bug-prone file blame.
|
|
346
|
-
|
|
335
|
+
|
|
347
336
|
Args:
|
|
348
337
|
stats: Precomputed statistics from precompute_coverage_stats()
|
|
349
338
|
bins: Number of histogram bins
|
|
@@ -391,3 +380,17 @@ def show_hal_histogram(
|
|
|
391
380
|
threshold = q3_b + 1.5 * (q3_b - q1_b)
|
|
392
381
|
_blame_hal(hal_files, threshold, blame_limit, width)
|
|
393
382
|
print(f"\n{'═' * width}")
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
# Re-export Halstead constants and helpers for backward compatibility
|
|
386
|
+
from .radon import ( # noqa: E402
|
|
387
|
+
_HAL_VOLUME,
|
|
388
|
+
_HAL_DIFFICULTY,
|
|
389
|
+
_HAL_EFFORT,
|
|
390
|
+
_HAL_BUGS,
|
|
391
|
+
_parse_hal_list,
|
|
392
|
+
_parse_hal_dict,
|
|
393
|
+
_parse_hal_entry,
|
|
394
|
+
_run_hal_chunk,
|
|
395
|
+
run_radon_json,
|
|
396
|
+
)
|
|
@@ -8,10 +8,26 @@ from pathlib import Path
|
|
|
8
8
|
# Cyclomatic Complexity rank thresholds (McCabe scale)
|
|
9
9
|
# A: 1–5 B: 6–10 C: 11–15 D: 16–20 E: 21–25 F: 26+
|
|
10
10
|
_CC_RISK = {
|
|
11
|
-
1: "A",
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
1: "A",
|
|
12
|
+
2: "A",
|
|
13
|
+
3: "A",
|
|
14
|
+
4: "A",
|
|
15
|
+
5: "A",
|
|
16
|
+
6: "B",
|
|
17
|
+
7: "B",
|
|
18
|
+
8: "B",
|
|
19
|
+
9: "B",
|
|
20
|
+
10: "B",
|
|
21
|
+
11: "C",
|
|
22
|
+
12: "C",
|
|
23
|
+
13: "C",
|
|
24
|
+
14: "C",
|
|
25
|
+
15: "C",
|
|
26
|
+
16: "D",
|
|
27
|
+
17: "D",
|
|
28
|
+
18: "D",
|
|
29
|
+
19: "D",
|
|
30
|
+
20: "D",
|
|
15
31
|
}
|
|
16
32
|
|
|
17
33
|
|
|
@@ -65,6 +81,7 @@ def run_radon_json(subcmd: str, root: str, extra_flags: list = None, debug: bool
|
|
|
65
81
|
print(f"⚠️ radon {subcmd} failed: {e}")
|
|
66
82
|
if debug:
|
|
67
83
|
import traceback
|
|
84
|
+
|
|
68
85
|
traceback.print_exc()
|
|
69
86
|
return {}
|
|
70
87
|
|
|
@@ -247,8 +264,7 @@ def run_radon_hal(root: str, debug: bool = False) -> dict:
|
|
|
247
264
|
print(f"⚠️ radon hal: path does not exist: {root_path}")
|
|
248
265
|
return {}
|
|
249
266
|
py_files = sorted(
|
|
250
|
-
str(p) for p in root_path.rglob("*.py")
|
|
251
|
-
if not any(part.startswith(".") for part in p.parts)
|
|
267
|
+
str(p) for p in root_path.rglob("*.py") if not any(part.startswith(".") for part in p.parts)
|
|
252
268
|
)
|
|
253
269
|
if not py_files:
|
|
254
270
|
print(f"⚠️ radon hal: no .py files found under {root_path}")
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"""File size distribution analysis."""
|
|
2
2
|
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
from .utils import ascii_histogram, blame_header, percentile
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
def show_file_size_distribution(
|
|
@@ -15,7 +16,7 @@ def show_file_size_distribution(
|
|
|
15
16
|
width: int = 80,
|
|
16
17
|
):
|
|
17
18
|
"""Display file size distribution histogram.
|
|
18
|
-
|
|
19
|
+
|
|
19
20
|
Args:
|
|
20
21
|
root: Root directory to scan
|
|
21
22
|
extension: File extension to analyze
|
|
@@ -88,7 +89,10 @@ def show_file_size_distribution(
|
|
|
88
89
|
[(lc, fp) for lc, fp in files if lc > iqr_lines], key=lambda x: x[0], reverse=True
|
|
89
90
|
)
|
|
90
91
|
blame_header(
|
|
91
|
-
f"size outliers Q3 + 1.5×IQR > {iqr_lines} lines",
|
|
92
|
+
f"size outliers Q3 + 1.5×IQR > {iqr_lines} lines",
|
|
93
|
+
len(blamed_files),
|
|
94
|
+
blame_limit,
|
|
95
|
+
width,
|
|
92
96
|
)
|
|
93
97
|
display = blamed_files if not blame_limit else blamed_files[:blame_limit]
|
|
94
98
|
if display:
|
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
from .utils import ascii_histogram, blame_header, percentile
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
def analyze_tree(root_path: str, bins: int = 10, blame_limit: int = 20, width: int = 80):
|
|
9
10
|
"""Analyze file tree structure, collecting metrics per directory.
|
|
10
|
-
|
|
11
|
+
|
|
11
12
|
Displays statistics and histograms about file and folder distribution.
|
|
12
|
-
|
|
13
|
+
|
|
13
14
|
Args:
|
|
14
15
|
root_path: Root directory to analyze
|
|
15
16
|
bins: Number of histogram bins
|
|
@@ -132,7 +133,10 @@ def analyze_tree(root_path: str, bins: int = 10, blame_limit: int = 20, width: i
|
|
|
132
133
|
)
|
|
133
134
|
|
|
134
135
|
blame_header(
|
|
135
|
-
f"subfolder count outliers (Q3+1.5×IQR > {boundary_d:g})",
|
|
136
|
+
f"subfolder count outliers (Q3+1.5×IQR > {boundary_d:g})",
|
|
137
|
+
len(blamed_d),
|
|
138
|
+
blame_limit,
|
|
139
|
+
width,
|
|
136
140
|
)
|
|
137
141
|
display_d = blamed_d if not blame_limit else blamed_d[:blame_limit]
|
|
138
142
|
if display_d:
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"""Shared utility functions for codebase analysis."""
|
|
2
2
|
|
|
3
|
-
import math
|
|
4
3
|
|
|
5
4
|
|
|
6
5
|
def percentile(sorted_vals: list, pct: int):
|
|
@@ -12,10 +11,10 @@ def percentile(sorted_vals: list, pct: int):
|
|
|
12
11
|
|
|
13
12
|
def format_line_ranges(lines: list) -> str:
|
|
14
13
|
"""Convert a list of line numbers into a human-readable ranges string.
|
|
15
|
-
|
|
14
|
+
|
|
16
15
|
Args:
|
|
17
16
|
lines: List of line numbers
|
|
18
|
-
|
|
17
|
+
|
|
19
18
|
Returns:
|
|
20
19
|
String like "1-5, 10, 15-18"
|
|
21
20
|
"""
|
|
@@ -37,7 +36,7 @@ def ascii_histogram(
|
|
|
37
36
|
counts: list, labels: list, bar_width: int = 36, suffixes: list = None, width: int = 80
|
|
38
37
|
) -> None:
|
|
39
38
|
"""Print a fixed-width ASCII histogram.
|
|
40
|
-
|
|
39
|
+
|
|
41
40
|
Args:
|
|
42
41
|
counts: List of counts for each bar
|
|
43
42
|
labels: List of labels for each bar
|
|
@@ -57,7 +56,7 @@ def ascii_histogram(
|
|
|
57
56
|
|
|
58
57
|
def blame_header(label: str, total: int, limit: int, width: int = 80) -> None:
|
|
59
58
|
"""Print a blame section header.
|
|
60
|
-
|
|
59
|
+
|
|
61
60
|
Args:
|
|
62
61
|
label: Description of what's being blamed
|
|
63
62
|
total: Total number of blamed items
|
|
@@ -72,7 +71,7 @@ def blame_header(label: str, total: int, limit: int, width: int = 80) -> None:
|
|
|
72
71
|
|
|
73
72
|
def fmt_seconds(seconds: float) -> str:
|
|
74
73
|
"""Format seconds into human-readable time string.
|
|
75
|
-
|
|
74
|
+
|
|
76
75
|
Examples:
|
|
77
76
|
0.5 -> "500ms"
|
|
78
77
|
1.5 -> "1.50s"
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "codebase-stats"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.2"
|
|
8
8
|
description = "Comprehensive codebase analysis library with coverage, metrics, and test duration analysis"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -102,7 +102,6 @@ ignore = [
|
|
|
102
102
|
|
|
103
103
|
[tool.ruff.lint.isort]
|
|
104
104
|
known-first-party = ["codebase_stats"]
|
|
105
|
-
profile = "black"
|
|
106
105
|
|
|
107
106
|
[tool.mypy]
|
|
108
107
|
python_version = "3.9"
|
|
@@ -112,7 +111,7 @@ disallow_untyped_defs = false
|
|
|
112
111
|
ignore_missing_imports = true
|
|
113
112
|
|
|
114
113
|
[project.scripts]
|
|
115
|
-
codebase-stats = "cli:main"
|
|
114
|
+
codebase-stats = "codebase_stats.cli:main"
|
|
116
115
|
|
|
117
116
|
[tool.setuptools]
|
|
118
117
|
packages = ["codebase_stats"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|