"""Generate formatted text reports for differ results.""" import datetime import os def generate_differ_report(result, profile_config, baseline_id, output_path): """ Generate a well-formatted text report for differ results. Args: result: dict with differ results (from _current_results) profile_config: dict with profile settings (paths, languages, exclude_patterns) baseline_id: str, baseline identifier output_path: str, path where to save the report """ lines = [] # Header lines.append("=" * 80) lines.append("PyUcc - Code Difference Report".center(80)) lines.append("=" * 80) lines.append("") # Timestamp timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") lines.append(f"Generated: {timestamp}") lines.append(f"Baseline ID: {baseline_id}") lines.append("") # Profile Configuration lines.append("-" * 80) lines.append("Profile Configuration") lines.append("-" * 80) lines.append(f"Profile Name: {profile_config.get('name', 'N/A')}") lines.append(f"Project Root: {profile_config.get('root', 'N/A')}") lines.append("") paths = profile_config.get("paths", []) if paths: lines.append(f"Included Paths ({len(paths)}):") for p in paths: lines.append(f" - {p}") else: lines.append("Included Paths: All") lines.append("") languages = profile_config.get("languages", []) if languages: lines.append(f"Languages ({len(languages)}):") lang_str = ", ".join(languages) # Wrap at 70 chars if len(lang_str) > 70: lines.append(f" {lang_str[:70]}") lines.append(f" {lang_str[70:]}") else: lines.append(f" {lang_str}") else: lines.append("Languages: All") lines.append("") exclude_patterns = profile_config.get("exclude_patterns", []) if exclude_patterns: lines.append(f"Exclude Patterns ({len(exclude_patterns)}):") for pat in exclude_patterns: lines.append(f" - {pat}") else: lines.append("Exclude Patterns: None") lines.append("") # Summary Statistics lines.append("-" * 80) lines.append("Summary Statistics") lines.append("-" * 80) total = result.get("total", {}) lines.append(f"Files Added: {total.get('added', 0):>8}") lines.append(f"Files Deleted: {total.get('deleted', 0):>8}") lines.append(f"Files Modified: {total.get('modified', 0):>8}") lines.append(f"Files Unmodified: {total.get('unmodified', 0):>8}") lines.append(f"{'─' * 30}") total_files = sum( [ total.get("added", 0), total.get("deleted", 0), total.get("modified", 0), total.get("unmodified", 0), ] ) lines.append(f"Total Files: {total_files:>8}") lines.append("") # Baseline vs Current metrics summary = result.get("summary", {}) if summary: baseline_counts = summary.get("baseline", {}).get("countings", {}) current_counts = summary.get("current", {}).get("countings", {}) baseline_metrics = summary.get("baseline", {}).get("metrics", {}) current_metrics = summary.get("current", {}).get("metrics", {}) lines.append("-" * 80) lines.append("Code Metrics Comparison") lines.append("-" * 80) lines.append(f"{'Metric':<30} {'Baseline':>15} {'Current':>15} {'Delta':>15}") lines.append("─" * 80) # Countings def _delta_str(baseline_val, current_val): delta = current_val - baseline_val sign = "+" if delta >= 0 else "" return f"{sign}{delta}" bc_code = baseline_counts.get("code_lines", 0) cc_code = current_counts.get("code_lines", 0) lines.append( f"{'Code Lines':<30} {bc_code:>15,} {cc_code:>15,} {_delta_str(bc_code, cc_code):>15}" ) bc_comment = baseline_counts.get("comment_lines", 0) cc_comment = current_counts.get("comment_lines", 0) lines.append( f"{'Comment Lines':<30} {bc_comment:>15,} {cc_comment:>15,} {_delta_str(bc_comment, cc_comment):>15}" ) bc_blank = baseline_counts.get("blank_lines", 0) cc_blank = current_counts.get("blank_lines", 0) lines.append( f"{'Blank Lines':<30} {bc_blank:>15,} {cc_blank:>15,} {_delta_str(bc_blank, cc_blank):>15}" ) bc_physical = baseline_counts.get("physical_lines", 0) cc_physical = current_counts.get("physical_lines", 0) lines.append( f"{'Physical Lines':<30} {bc_physical:>15,} {cc_physical:>15,} {_delta_str(bc_physical, cc_physical):>15}" ) lines.append("") # Metrics bm_func = baseline_metrics.get("total_func_count", 0) cm_func = current_metrics.get("total_func_count", 0) lines.append( f"{'Function Count':<30} {bm_func:>15,} {cm_func:>15,} {_delta_str(bm_func, cm_func):>15}" ) bm_avgcc = baseline_metrics.get("avg_avg_cc", 0.0) cm_avgcc = current_metrics.get("avg_avg_cc", 0.0) lines.append( f"{'Avg Cyclomatic Complexity':<30} {bm_avgcc:>15.2f} {cm_avgcc:>15.2f} {_delta_str(bm_avgcc, cm_avgcc):>15}" ) bm_mi = baseline_metrics.get("avg_mi", 0.0) cm_mi = current_metrics.get("avg_mi", 0.0) lines.append( f"{'Maintainability Index':<30} {bm_mi:>15.2f} {cm_mi:>15.2f} {_delta_str(bm_mi, cm_mi):>15}" ) lines.append("") # Detailed File List lines.append("-" * 80) lines.append("Detailed File Changes") lines.append("-" * 80) lines.append("") pairs = result.get("pairs", []) # Group by status added_files = [] deleted_files = [] modified_files = [] unmodified_files = [] for pair in pairs: counts = pair.get("counts", {}) if counts.get("added", 0) > 0: added_files.append(pair) elif counts.get("deleted", 0) > 0: deleted_files.append(pair) elif counts.get("modified", 0) > 0: modified_files.append(pair) else: unmodified_files.append(pair) # Added files if added_files: lines.append(f"Added Files ({len(added_files)}):") lines.append("─" * 80) for pair in added_files: current_name = pair.get("fileB", "") cd = pair.get("countings_delta") md = pair.get("metrics_delta") cur_m = pair.get("current_metrics") lines.append(f" + {current_name}") if cd: lines.append( f" Code: {cd.get('code_lines', 0):>6} Comment: {cd.get('comment_lines', 0):>6} Blank: {cd.get('blank_lines', 0):>6}" ) if cur_m: lines.append( f" Functions: {cur_m.get('func_count', 0):>4} Avg CC: {cur_m.get('avg_cc', 0.0):>6.2f} MI: {cur_m.get('mi', 0.0):>6.2f}" ) lines.append("") # Deleted files if deleted_files: lines.append(f"Deleted Files ({len(deleted_files)}):") lines.append("─" * 80) for pair in deleted_files: baseline_name = pair.get("fileA", "") lines.append(f" - {baseline_name}") lines.append("") # Modified files if modified_files: lines.append(f"Modified Files ({len(modified_files)}):") lines.append("─" * 80) for pair in modified_files: current_name = pair.get("fileB", "") baseline_name = pair.get("fileA", "") cd = pair.get("countings_delta") md = pair.get("metrics_delta") lines.append(f" M {current_name}") if baseline_name != current_name: lines.append(f" (was: {baseline_name})") # Always show countings delta, even if all zeros (file was modified at byte level) if cd: delta_code = cd.get("code_lines", 0) delta_comment = cd.get("comment_lines", 0) delta_blank = cd.get("blank_lines", 0) # Check if there are any non-zero deltas has_counting_changes = any( [delta_code != 0, delta_comment != 0, delta_blank != 0] ) if has_counting_changes: sign_code = "+" if delta_code >= 0 else "" sign_comment = "+" if delta_comment >= 0 else "" sign_blank = "+" if delta_blank >= 0 else "" lines.append( f" Code: {sign_code}{delta_code:>6} Comment: {sign_comment}{delta_comment:>6} Blank: {sign_blank}{delta_blank:>6}" ) else: lines.append( f" Code: + 0 Comment: + 0 Blank: + 0" ) # Always show metrics delta, even if all zeros if md: delta_func = md.get("func_count", 0) delta_avgcc = md.get("avg_cc", 0.0) delta_mi = md.get("mi", 0.0) # Check if there are any non-zero deltas has_metric_changes = any( [delta_func != 0, abs(delta_avgcc) > 0.01, abs(delta_mi) > 0.01] ) if has_metric_changes: sign_func = "+" if delta_func >= 0 else "" sign_avgcc = "+" if delta_avgcc >= 0 else "" sign_mi = "+" if delta_mi >= 0 else "" lines.append( f" Functions: {sign_func}{delta_func:>4} Avg CC: {sign_avgcc}{delta_avgcc:>6.2f} MI: {sign_mi}{delta_mi:>6.2f}" ) else: lines.append( f" Functions: + 0 Avg CC: + 0.00 MI: + 0.00" ) lines.append("") # Sort pairs by current file name for consistent ordering sorted_pairs = sorted( pairs, key=lambda p: p.get("fileB", p.get("fileA", "")).lower() ) # Also generate a compact UCC-style table (New | Deleted | Modified | Unmodified | Module) # with extra delta columns appended for richer information. # Insert an English legend explaining the table columns so the reader understands # each field before the compact table is shown. lines.append("") lines.append("Legend of table fields:") lines.append( " New : Number of lines added in this file compared to the baseline" ) lines.append(" Del : Number of lines deleted compared to the baseline") lines.append( " Mod : Number of lines modified (replaced) compared to the baseline" ) lines.append(" Unmod : Number of lines unchanged") lines.append( " St : File status (A=Added, D=Deleted, M=Modified, = = Unmodified)" ) lines.append(" ΔCode : Change in code lines (current - baseline)") lines.append(" ΔComm : Change in comment lines (current - baseline)") lines.append(" ΔBlank : Change in blank lines (current - baseline)") lines.append(" ΔFunc : Change in number of functions (current - baseline)") lines.append(" ΔAvgCC : Change in average cyclomatic complexity per function") lines.append(" ΔMI : Change in Maintainability Index (MI)") lines.append("") lines.append("UCC-Style Compact Table:") # Header with fixed column widths for good alignment header_cols = f"{'New':>3} | {'Del':>3} | {'Mod':>3} | {'Unmod':>7} | {'St':^3} | {'ΔCode':>6} | {'ΔComm':>6} | {'ΔBlank':>6} | {'ΔFunc':>5} | {'ΔAvgCC':>7} | {'ΔMI':>6} | Module" lines.append(header_cols) lines.append("-" * 140) # Use the same sorted_pairs order for pair in sorted_pairs: counts = pair.get("counts", {}) a = counts.get("added", 0) d = counts.get("deleted", 0) m = counts.get("modified", 0) u = counts.get("unmodified", 0) cd = pair.get("countings_delta") or {} md = pair.get("metrics_delta") or {} def fmt_int(v): try: return str(int(v)) except Exception: return "0" def fmt_float(v): try: return f"{float(v):.2f}" except Exception: return "0.00" row_mod = pair.get("fileB") or pair.get("fileA") or "" # Trim long module names similarly to UCC display if len(row_mod) > 80: row_mod = ".." + row_mod[-78:] # Determine status char similar to other parts of the report if a > 0: st = "A" elif d > 0: st = "D" elif m > 0: st = "M" else: st = "=" # Compose a well-aligned row using the same widths as the header line = ( f"{a:>3} | {d:>3} | {m:>3} | {u:>7} | {st:^3} | " f"{fmt_int(cd.get('code_lines')):>6} | {fmt_int(cd.get('comment_lines')):>6} | {fmt_int(cd.get('blank_lines')):>6} | {fmt_int(md.get('func_count')):>5} | {fmt_float(md.get('avg_cc')):>7} | {fmt_float(md.get('mi')):>6} | {row_mod}" ) lines.append(line) lines.append("") # Footer lines.append("=" * 80) lines.append("End of Report".center(80)) lines.append("=" * 80) # Write to file try: with open(output_path, "w", encoding="utf-8") as f: f.write("\n".join(lines)) return True except Exception as e: print(f"Failed to write report: {e}") return False