ci: Add DevOps quality assurance with pre-commit hooks and GitHub Actions
- Add pre-commit hooks: Ruff lint/format, Bandit security scan, YAML/TOML validation - Add GitHub Actions CI workflow: lint, security, test, and build jobs - Configure Ruff and Bandit in pyproject.toml - Add pytest test suite for build_epub.py (25 tests) - Fix code issues: exception chaining, httpx timeout, formatting - Add requirements.txt and requirements-dev.txt
This commit is contained in:
@@ -2,31 +2,33 @@
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
def analyze_code_metrics(code):
|
||||
"""Analyze code for common metrics."""
|
||||
|
||||
# Count functions
|
||||
functions = len(re.findall(r'^def\s+\w+', code, re.MULTILINE))
|
||||
functions = len(re.findall(r"^def\s+\w+", code, re.MULTILINE))
|
||||
|
||||
# Count classes
|
||||
classes = len(re.findall(r'^class\s+\w+', code, re.MULTILINE))
|
||||
classes = len(re.findall(r"^class\s+\w+", code, re.MULTILINE))
|
||||
|
||||
# Average line length
|
||||
lines = code.split('\n')
|
||||
lines = code.split("\n")
|
||||
avg_length = sum(len(l) for l in lines) / len(lines) if lines else 0
|
||||
|
||||
# Estimate complexity
|
||||
complexity = len(re.findall(r'\b(if|elif|else|for|while|and|or)\b', code))
|
||||
complexity = len(re.findall(r"\b(if|elif|else|for|while|and|or)\b", code))
|
||||
|
||||
return {
|
||||
'functions': functions,
|
||||
'classes': classes,
|
||||
'avg_line_length': avg_length,
|
||||
'complexity_score': complexity
|
||||
"functions": functions,
|
||||
"classes": classes,
|
||||
"avg_line_length": avg_length,
|
||||
"complexity_score": complexity,
|
||||
}
|
||||
|
||||
if __name__ == '__main__':
|
||||
with open(sys.argv[1], 'r') as f:
|
||||
|
||||
if __name__ == "__main__":
|
||||
with open(sys.argv[1]) as f:
|
||||
code = f.read()
|
||||
metrics = analyze_code_metrics(code)
|
||||
for key, value in metrics.items():
|
||||
|
||||
@@ -6,14 +6,14 @@ Helps identify if refactoring actually simplifies code structure.
|
||||
|
||||
import re
|
||||
import sys
|
||||
from typing import Dict, Tuple
|
||||
|
||||
|
||||
class ComplexityAnalyzer:
|
||||
"""Analyze code complexity metrics."""
|
||||
|
||||
def __init__(self, code: str):
|
||||
self.code = code
|
||||
self.lines = code.split('\n')
|
||||
self.lines = code.split("\n")
|
||||
|
||||
def calculate_cyclomatic_complexity(self) -> int:
|
||||
"""
|
||||
@@ -24,13 +24,13 @@ class ComplexityAnalyzer:
|
||||
|
||||
# Count decision points
|
||||
decision_patterns = [
|
||||
r'\bif\b',
|
||||
r'\belif\b',
|
||||
r'\bfor\b',
|
||||
r'\bwhile\b',
|
||||
r'\bexcept\b',
|
||||
r'\band\b(?!$)',
|
||||
r'\bor\b(?!$)'
|
||||
r"\bif\b",
|
||||
r"\belif\b",
|
||||
r"\bfor\b",
|
||||
r"\bwhile\b",
|
||||
r"\bexcept\b",
|
||||
r"\band\b(?!$)",
|
||||
r"\bor\b(?!$)",
|
||||
]
|
||||
|
||||
for pattern in decision_patterns:
|
||||
@@ -49,10 +49,10 @@ class ComplexityAnalyzer:
|
||||
|
||||
for line in self.lines:
|
||||
# Track nesting depth
|
||||
if re.search(r'^\s*(if|for|while|def|class|try)\b', line):
|
||||
if re.search(r"^\s*(if|for|while|def|class|try)\b", line):
|
||||
nesting_depth += 1
|
||||
cognitive += nesting_depth
|
||||
elif re.search(r'^\s*(elif|else|except|finally)\b', line):
|
||||
elif re.search(r"^\s*(elif|else|except|finally)\b", line):
|
||||
cognitive += nesting_depth
|
||||
|
||||
# Reduce nesting when unindenting
|
||||
@@ -74,28 +74,37 @@ class ComplexityAnalyzer:
|
||||
cognitive = self.calculate_cognitive_complexity()
|
||||
|
||||
# Simplified MI calculation
|
||||
mi = 171 - 5.2 * (cyclomatic / lines) - 0.23 * (cognitive) - 16.2 * (lines / 1000)
|
||||
mi = (
|
||||
171
|
||||
- 5.2 * (cyclomatic / lines)
|
||||
- 0.23 * (cognitive)
|
||||
- 16.2 * (lines / 1000)
|
||||
)
|
||||
|
||||
return max(0, min(100, mi))
|
||||
|
||||
def get_complexity_report(self) -> Dict:
|
||||
def get_complexity_report(self) -> dict:
|
||||
"""Generate comprehensive complexity report."""
|
||||
return {
|
||||
'cyclomatic_complexity': self.calculate_cyclomatic_complexity(),
|
||||
'cognitive_complexity': self.calculate_cognitive_complexity(),
|
||||
'maintainability_index': round(self.calculate_maintainability_index(), 2),
|
||||
'lines_of_code': len(self.lines),
|
||||
'avg_line_length': round(sum(len(l) for l in self.lines) / len(self.lines), 2) if self.lines else 0
|
||||
"cyclomatic_complexity": self.calculate_cyclomatic_complexity(),
|
||||
"cognitive_complexity": self.calculate_cognitive_complexity(),
|
||||
"maintainability_index": round(self.calculate_maintainability_index(), 2),
|
||||
"lines_of_code": len(self.lines),
|
||||
"avg_line_length": round(
|
||||
sum(len(l) for l in self.lines) / len(self.lines), 2
|
||||
)
|
||||
if self.lines
|
||||
else 0,
|
||||
}
|
||||
|
||||
|
||||
def compare_files(before_file: str, after_file: str) -> None:
|
||||
"""Compare complexity metrics between two code versions."""
|
||||
|
||||
with open(before_file, 'r') as f:
|
||||
with open(before_file) as f:
|
||||
before_code = f.read()
|
||||
|
||||
with open(after_file, 'r') as f:
|
||||
with open(after_file) as f:
|
||||
after_code = f.read()
|
||||
|
||||
before_analyzer = ComplexityAnalyzer(before_code)
|
||||
@@ -123,10 +132,16 @@ def compare_files(before_file: str, after_file: str) -> None:
|
||||
print(f" Avg Line Length: {after_metrics['avg_line_length']}")
|
||||
|
||||
print("\nCHANGES:")
|
||||
cyclomatic_change = after_metrics['cyclomatic_complexity'] - before_metrics['cyclomatic_complexity']
|
||||
cognitive_change = after_metrics['cognitive_complexity'] - before_metrics['cognitive_complexity']
|
||||
mi_change = after_metrics['maintainability_index'] - before_metrics['maintainability_index']
|
||||
loc_change = after_metrics['lines_of_code'] - before_metrics['lines_of_code']
|
||||
cyclomatic_change = (
|
||||
after_metrics["cyclomatic_complexity"] - before_metrics["cyclomatic_complexity"]
|
||||
)
|
||||
cognitive_change = (
|
||||
after_metrics["cognitive_complexity"] - before_metrics["cognitive_complexity"]
|
||||
)
|
||||
mi_change = (
|
||||
after_metrics["maintainability_index"] - before_metrics["maintainability_index"]
|
||||
)
|
||||
loc_change = after_metrics["lines_of_code"] - before_metrics["lines_of_code"]
|
||||
|
||||
print(f" Cyclomatic Complexity: {cyclomatic_change:+d}")
|
||||
print(f" Cognitive Complexity: {cognitive_change:+d}")
|
||||
@@ -151,7 +166,7 @@ def compare_files(before_file: str, after_file: str) -> None:
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 3:
|
||||
print("Usage: python compare-complexity.py <before_file> <after_file>")
|
||||
sys.exit(1)
|
||||
|
||||
Reference in New Issue
Block a user