Coverage for src/diff_engine.py: 98%

49 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2025-10-08 05:40 +0000

1#!/usr/bin/env python3 

2 

3import difflib 

4from typing import List, Dict, Any 

5from dataclasses import dataclass 

6 

7@dataclass 

8class DiffLine: 

9 line_type: str # 'added', 'removed', 'unchanged' 

10 content: str 

11 line_number: int 

12 

13class DiffEngine: 

14 """Simple diff engine for content changes""" 

15 

16 def __init__(self): 

17 self.previous_content = {} 

18 

19 def compare_content(self, section_id: str, old_content: str, new_content: str) -> Dict[str, Any]: 

20 """Compare two versions of content and return diff""" 

21 

22 old_lines = old_content.splitlines() if old_content else [] 

23 new_lines = new_content.splitlines() if new_content else [] 

24 

25 diff_lines = [] 

26 

27 # Use difflib for line-by-line comparison 

28 differ = difflib.unified_diff( 

29 old_lines, 

30 new_lines, 

31 fromfile='old', 

32 tofile='new', 

33 lineterm='' 

34 ) 

35 

36 changes = { 

37 'added_lines': 0, 

38 'removed_lines': 0, 

39 'changed_lines': 0 

40 } 

41 

42 line_num = 0 

43 for line in differ: 

44 line_num += 1 

45 if line.startswith('+') and not line.startswith('+++'): 

46 diff_lines.append(DiffLine('added', line[1:], line_num)) 

47 changes['added_lines'] += 1 

48 elif line.startswith('-') and not line.startswith('---'): 

49 diff_lines.append(DiffLine('removed', line[1:], line_num)) 

50 changes['removed_lines'] += 1 

51 elif not line.startswith('@@') and not line.startswith('+++') and not line.startswith('---'): 

52 diff_lines.append(DiffLine('unchanged', line[1:] if line.startswith(' ') else line, line_num)) 

53 

54 # Calculate change percentage 

55 total_lines = max(len(old_lines), len(new_lines)) 

56 change_percentage = ((changes['added_lines'] + changes['removed_lines']) / total_lines * 100) if total_lines > 0 else 0 

57 

58 return { 

59 'section_id': section_id, 

60 'changes': changes, 

61 'change_percentage': round(change_percentage, 2), 

62 'diff_lines': [{'type': d.line_type, 'content': d.content, 'line_number': d.line_number} for d in diff_lines], 

63 'has_changes': changes['added_lines'] > 0 or changes['removed_lines'] > 0 

64 } 

65 

66 def track_change(self, section_id: str, content: str) -> Dict[str, Any]: 

67 """Track a change and return diff from previous version""" 

68 old_content = self.previous_content.get(section_id, '') 

69 diff_result = self.compare_content(section_id, old_content, content) 

70 

71 # Store new content for next comparison 

72 self.previous_content[section_id] = content 

73 

74 return diff_result 

75 

76 def get_html_diff(self, section_id: str, old_content: str, new_content: str) -> str: 

77 """Generate HTML diff visualization""" 

78 diff_result = self.compare_content(section_id, old_content, new_content) 

79 

80 html_lines = [] 

81 html_lines.append('<div class="diff-container">') 

82 html_lines.append(f'<h4>Changes in {section_id}</h4>') 

83 html_lines.append(f'<p>Added: {diff_result["changes"]["added_lines"]} lines, ' 

84 f'Removed: {diff_result["changes"]["removed_lines"]} lines, ' 

85 f'Change: {diff_result["change_percentage"]}%</p>') 

86 

87 for diff_line in diff_result['diff_lines']: 

88 css_class = { 

89 'added': 'diff-added', 

90 'removed': 'diff-removed', 

91 'unchanged': 'diff-unchanged' 

92 }.get(diff_line['type'], '') 

93 

94 html_lines.append(f'<div class="{css_class}">{diff_line["content"]}</div>') 

95 

96 html_lines.append('</div>') 

97 return '\n'.join(html_lines) 

98 

99 def get_summary(self) -> Dict[str, Any]: 

100 """Get summary of all tracked changes""" 

101 return { 

102 'tracked_sections': len(self.previous_content), 

103 'sections': list(self.previous_content.keys()) 

104 }