Skip to content

Commit a84a9ef

Browse files
committed
debug
1 parent c510498 commit a84a9ef

File tree

3 files changed

+1098
-0
lines changed

3 files changed

+1098
-0
lines changed
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Compare Python dependency pinning between baseline (master) and PR branches.
4+
5+
This script reads dep-analyzer.py JSON outputs from both branches and identifies:
6+
- New unpinned dependencies (no version constraint)
7+
- New lower-bound only dependencies (e.g., >=2.0 without upper bound)
8+
- Downgraded dependencies (from stricter to looser pinning)
9+
10+
Usage:
11+
check_python_deps.py --baseline-file /tmp/master-project.json \
12+
--pr-file /tmp/pr-project.json \
13+
--project-name metadata-ingestion
14+
15+
Exit codes:
16+
0: No violations found
17+
1: Violations found
18+
2: Error reading or parsing files
19+
"""
20+
import argparse
21+
import json
22+
import sys
23+
from pathlib import Path
24+
from typing import Any
25+
26+
27+
# Pin levels that should cause failure if newly introduced
28+
VIOLATION_LEVELS = {"unpinned", "lower_bound"}
29+
30+
31+
def load_json_results(file_path: Path) -> dict[str, Any] | None:
32+
"""
33+
Load and parse dep-analyzer.py JSON output.
34+
35+
Returns None if file doesn't exist (project added/deleted in PR).
36+
Raises exception if file exists but is malformed.
37+
"""
38+
if not file_path.exists():
39+
return None
40+
41+
try:
42+
with open(file_path) as f:
43+
data = json.load(f)
44+
return data
45+
except json.JSONDecodeError as e:
46+
raise ValueError(f"Malformed JSON in {file_path}: {e}")
47+
48+
49+
def build_dependency_map(data: dict[str, Any] | None) -> dict[str, dict[str, Any]]:
50+
"""
51+
Build a mapping from dependency name to its analysis.
52+
53+
Returns empty dict if data is None.
54+
"""
55+
if data is None:
56+
return {}
57+
58+
dep_map = {}
59+
for level, deps in data.get("by_level", {}).items():
60+
for dep in deps:
61+
dep_map[dep["name"]] = {
62+
"level": level,
63+
"specifier": dep.get("specifier", ""),
64+
"source": dep.get("source", "unknown")
65+
}
66+
67+
return dep_map
68+
69+
70+
def classify_violation(
71+
name: str,
72+
baseline_info: dict[str, Any] | None,
73+
pr_info: dict[str, Any]
74+
) -> dict[str, Any] | None:
75+
"""
76+
Classify if a dependency represents a violation.
77+
78+
Returns violation dict if it's a violation, None otherwise.
79+
"""
80+
pr_level = pr_info["level"]
81+
82+
# Check if PR has a problematic pin level
83+
if pr_level not in VIOLATION_LEVELS:
84+
return None
85+
86+
# Case 1: New dependency with bad pinning
87+
if baseline_info is None:
88+
violation_type = f"new_{pr_level}"
89+
return {
90+
"type": violation_type,
91+
"name": name,
92+
"old_spec": None,
93+
"old_level": None,
94+
"new_spec": pr_info["specifier"],
95+
"new_level": pr_level,
96+
"source": pr_info["source"]
97+
}
98+
99+
# Case 2: Existing dependency downgraded to bad pinning
100+
baseline_level = baseline_info["level"]
101+
if baseline_level not in VIOLATION_LEVELS:
102+
# Was good, now bad - downgrade violation
103+
return {
104+
"type": "downgraded",
105+
"name": name,
106+
"old_spec": baseline_info["specifier"],
107+
"old_level": baseline_level,
108+
"new_spec": pr_info["specifier"],
109+
"new_level": pr_level,
110+
"source": pr_info["source"]
111+
}
112+
113+
# Case 3: Was already bad in baseline - grandfathered
114+
return None
115+
116+
117+
def compare_dependencies(
118+
baseline_map: dict[str, dict[str, Any]],
119+
pr_map: dict[str, dict[str, Any]]
120+
) -> list[dict[str, Any]]:
121+
"""
122+
Compare baseline and PR dependency maps to find violations.
123+
124+
Returns list of violation dictionaries.
125+
"""
126+
violations = []
127+
128+
for name, pr_info in pr_map.items():
129+
baseline_info = baseline_map.get(name)
130+
violation = classify_violation(name, baseline_info, pr_info)
131+
if violation:
132+
violations.append(violation)
133+
134+
return violations
135+
136+
137+
def generate_summary(violations: list[dict[str, Any]]) -> dict[str, int]:
138+
"""Generate violation summary statistics."""
139+
summary = {
140+
"new_unpinned": 0,
141+
"new_lower_bound": 0,
142+
"downgraded": 0
143+
}
144+
145+
for v in violations:
146+
vtype = v["type"]
147+
if vtype == "new_unpinned":
148+
summary["new_unpinned"] += 1
149+
elif vtype == "new_lower_bound":
150+
summary["new_lower_bound"] += 1
151+
elif vtype == "downgraded":
152+
summary["downgraded"] += 1
153+
154+
return summary
155+
156+
157+
def main():
158+
parser = argparse.ArgumentParser(
159+
description="Compare Python dependency pinning between branches"
160+
)
161+
parser.add_argument(
162+
"--baseline-file",
163+
type=Path,
164+
required=True,
165+
help="Path to master branch dep-analyzer JSON output"
166+
)
167+
parser.add_argument(
168+
"--pr-file",
169+
type=Path,
170+
required=True,
171+
help="Path to PR branch dep-analyzer JSON output"
172+
)
173+
parser.add_argument(
174+
"--project-name",
175+
required=True,
176+
help="Name of the project being compared (for reporting)"
177+
)
178+
179+
args = parser.parse_args()
180+
181+
try:
182+
# Load both JSON files
183+
baseline_data = load_json_results(args.baseline_file)
184+
pr_data = load_json_results(args.pr_file)
185+
186+
# Handle edge cases
187+
if pr_data is None:
188+
# Project deleted in PR - no deps to check
189+
result = {
190+
"project": args.project_name,
191+
"has_violations": False,
192+
"violations": [],
193+
"summary": {"new_unpinned": 0, "new_lower_bound": 0, "downgraded": 0},
194+
"note": "Project deleted in PR"
195+
}
196+
print(json.dumps(result, indent=2))
197+
return 0
198+
199+
# Build dependency maps
200+
baseline_map = build_dependency_map(baseline_data)
201+
pr_map = build_dependency_map(pr_data)
202+
203+
# Compare and find violations
204+
violations = compare_dependencies(baseline_map, pr_map)
205+
summary = generate_summary(violations)
206+
207+
# Generate output
208+
result = {
209+
"project": args.project_name,
210+
"has_violations": len(violations) > 0,
211+
"violations": violations,
212+
"summary": summary
213+
}
214+
215+
if baseline_data is None:
216+
result["note"] = "New project in PR - all dependencies treated as new"
217+
218+
print(json.dumps(result, indent=2))
219+
220+
# Exit with appropriate code
221+
return 1 if violations else 0
222+
223+
except ValueError as e:
224+
# JSON parsing or other validation error
225+
error_result = {
226+
"project": args.project_name,
227+
"error": str(e),
228+
"has_violations": False,
229+
"violations": []
230+
}
231+
print(json.dumps(error_result, indent=2), file=sys.stderr)
232+
return 2
233+
234+
except Exception as e:
235+
# Unexpected error
236+
error_result = {
237+
"project": args.project_name,
238+
"error": f"Unexpected error: {e}",
239+
"has_violations": False,
240+
"violations": []
241+
}
242+
print(json.dumps(error_result, indent=2), file=sys.stderr)
243+
return 2
244+
245+
246+
if __name__ == "__main__":
247+
sys.exit(main())

0 commit comments

Comments
 (0)