driftfix 0.1.0__py3-none-any.whl

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.
driftfix/__init__.py ADDED
File without changes
driftfix/cli.py ADDED
@@ -0,0 +1,138 @@
1
+ """driftfix: a dependency upgrade broke your tests, Claude fixes the code."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import asyncio
7
+ import subprocess
8
+ import sys
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+
12
+ # Pinning back to the old version is not a fix.
13
+ MANIFESTS = {
14
+ "requirements.txt", "pyproject.toml", "setup.py", "setup.cfg", "Pipfile", "Pipfile.lock",
15
+ "poetry.lock", "uv.lock", "package.json", "package-lock.json", "yarn.lock", "pnpm-lock.yaml",
16
+ }
17
+
18
+ PROMPT = """\
19
+ The dependency `{package}` was upgraded{versions}. The test suite now fails:
20
+
21
+ $ {test_command}
22
+ {output}
23
+
24
+ Fix this repository's code so it works with the new version of `{package}`.
25
+ - Read the package's changelog / migration guide for the relevant versions if you need to.
26
+ - Change application code. Change tests only where they use `{package}`'s API directly.
27
+ - Never pin, downgrade, or edit dependency files ({manifests}).
28
+ - Never delete or skip tests to make them pass.
29
+ - Run `{test_command}` to confirm the fix.
30
+ Your final message must be only a short markdown summary of what changed and why, for a PR description."""
31
+
32
+
33
+ @dataclass
34
+ class TestRun:
35
+ passed: bool
36
+ output: str
37
+
38
+
39
+ def run_tests(command: str, repo: Path, timeout: int) -> TestRun:
40
+ try:
41
+ proc = subprocess.run(
42
+ command, shell=True, cwd=repo, capture_output=True, text=True, timeout=timeout
43
+ )
44
+ except subprocess.TimeoutExpired:
45
+ return TestRun(False, f"timed out after {timeout}s")
46
+ lines = (proc.stdout + proc.stderr).splitlines()
47
+ return TestRun(proc.returncode == 0, "\n".join(lines[-150:]))
48
+
49
+
50
+ def changed_files(repo: Path) -> list[str]:
51
+ out = subprocess.run(
52
+ ["git", "status", "--porcelain"], cwd=repo, capture_output=True, text=True, check=True
53
+ ).stdout
54
+ return [line[3:].split(" -> ")[-1] for line in out.splitlines()]
55
+
56
+
57
+ def build_prompt(package: str, old: str | None, new: str | None, command: str, out: str) -> str:
58
+ versions = f" from {old or '?'} to {new}" if new else (f" from {old}" if old else "")
59
+ return PROMPT.format(
60
+ package=package, versions=versions, test_command=command, output=out,
61
+ manifests=", ".join(sorted(MANIFESTS)),
62
+ )
63
+
64
+
65
+ async def run_agent(prompt: str, repo: Path, model: str, max_turns: int, budget: float) -> tuple[str, float]:
66
+ from claude_agent_sdk import ClaudeAgentOptions, ResultMessage, query
67
+
68
+ options = ClaudeAgentOptions(
69
+ cwd=repo,
70
+ model=model,
71
+ allowed_tools=["Read", "Edit", "Write", "Glob", "Grep", "Bash", "WebFetch", "WebSearch"],
72
+ permission_mode="bypassPermissions", # headless; run in CI or a throwaway checkout
73
+ max_turns=max_turns,
74
+ max_budget_usd=budget,
75
+ setting_sources=["project"], # repo's CLAUDE.md, not the user's personal config
76
+ skills=[],
77
+ )
78
+ summary, cost = "", 0.0
79
+ async for message in query(prompt=prompt, options=options):
80
+ if isinstance(message, ResultMessage):
81
+ summary, cost = message.result or "", message.total_cost_usd or 0.0
82
+ return summary, cost
83
+
84
+
85
+ def fix(args: argparse.Namespace) -> int:
86
+ repo = args.repo.resolve()
87
+ if changed_files(repo):
88
+ print("Working tree is dirty. Commit or stash first so driftfix's edits are reviewable.")
89
+ return 2
90
+ before = run_tests(args.test, repo, args.timeout)
91
+ if before.passed:
92
+ print(f"Tests pass with {args.package} as installed. Nothing to fix.")
93
+ return 0
94
+ baseline = set(changed_files(repo)) # test-run artifacts like __pycache__
95
+
96
+ print(f"Tests fail after upgrading {args.package}. Handing off to Claude ({args.model})...")
97
+ prompt = build_prompt(args.package, args.from_version, args.to_version, args.test, before.output)
98
+ try:
99
+ summary, cost = asyncio.run(run_agent(prompt, repo, args.model, args.max_turns, args.budget))
100
+ except Exception as exc: # still report on the PR instead of dying silently
101
+ summary, cost = f"Agent error: `{exc}`", 0.0
102
+
103
+ touched = [f for f in changed_files(repo) if f not in baseline]
104
+ pinned = [f for f in touched if Path(f).name in MANIFESTS]
105
+ after = run_tests(args.test, repo, args.timeout) # don't trust the agent's word
106
+ ok = after.passed and bool(touched) and not pinned
107
+
108
+ status = "✅ Fixed" if ok else "❌ Not fixed"
109
+ report = f"## driftfix: {args.package}\n\n**{status}** · cost ${cost:.2f}\n\n{summary}\n"
110
+ if pinned:
111
+ report += f"\nRejected: agent edited dependency files {pinned}.\n"
112
+ if not after.passed:
113
+ report += f"\nTests still failing:\n```\n{after.output[-3000:]}\n```\n"
114
+ print(report)
115
+ if args.summary:
116
+ args.summary.write_text(report)
117
+ return 0 if ok else 1
118
+
119
+
120
+ def main(argv: list[str] | None = None) -> int:
121
+ p = argparse.ArgumentParser(prog="driftfix", description=__doc__)
122
+ sub = p.add_subparsers(dest="command", required=True)
123
+ f = sub.add_parser("fix", help="fix code broken by a dependency upgrade")
124
+ f.add_argument("--package", required=True)
125
+ f.add_argument("--from", dest="from_version")
126
+ f.add_argument("--to", dest="to_version")
127
+ f.add_argument("--test", default="pytest -q", help="test command (default: pytest -q)")
128
+ f.add_argument("--repo", type=Path, default=Path("."))
129
+ f.add_argument("--model", default="claude-opus-5")
130
+ f.add_argument("--max-turns", type=int, default=50)
131
+ f.add_argument("--budget", type=float, default=5.0, help="max spend in USD (default: 5)")
132
+ f.add_argument("--timeout", type=int, default=600, help="test timeout in seconds")
133
+ f.add_argument("--summary", type=Path, help="write the markdown report here")
134
+ return fix(p.parse_args(argv))
135
+
136
+
137
+ if __name__ == "__main__":
138
+ sys.exit(main())
@@ -0,0 +1,104 @@
1
+ Metadata-Version: 2.5
2
+ Name: driftfix
3
+ Version: 0.1.0
4
+ Summary: Dependabot bumps it. CI breaks. driftfix fixes the code.
5
+ Project-URL: Repository, https://github.com/Gthejesraj/driftfix
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.10
9
+ Requires-Dist: claude-agent-sdk>=0.2
10
+ Provides-Extra: dev
11
+ Requires-Dist: pytest>=8; extra == 'dev'
12
+ Description-Content-Type: text/markdown
13
+
14
+ # driftfix
15
+
16
+ **Dependabot bumps it. CI breaks. driftfix fixes the code.**
17
+
18
+ Dependency bots open the upgrade PR, then leave you to fix whatever broke.
19
+ driftfix finishes the job: it reads the failing tests, has Claude adapt your
20
+ code to the new version, reruns your tests itself, and pushes the fix onto
21
+ the same PR. If it can't get to green, it comments what it tried and commits
22
+ nothing.
23
+
24
+ ```
25
+ Dependabot PR (openai 0.28 → 1.x) → tests fail → Claude fixes code → tests rerun → ✅ commit on the PR
26
+ └→ ❌ comment only
27
+ ```
28
+
29
+ ## Benchmark
30
+
31
+ Real breaking upgrades, each a small app with tests that pass on the old version and fail on the new one ([`bench/`](bench/)):
32
+
33
+ | Package | Upgrade | Fixed | Cost | Diff |
34
+ |---|---|---|---|---|
35
+ | httpx | 0.27.2 → 0.28.1 | ✅ | $0.12 | 1 file changed, 4 insertions(+), 1 deletion(-) |
36
+ | numpy | 1.26.4 → 2.5.3 | ✅ | $0.14 | 1 file changed, 5 insertions(+), 5 deletions(-) |
37
+ | openai | 0.28.1 → 3.24.0 | ✅ | $0.29 | 1 file changed, 4 insertions(+), 3 deletions(-) |
38
+ | pydantic | 1.10.21 → 2.13.5 | ✅ | $0.20 | 1 file changed, 11 insertions(+), 11 deletions(-) |
39
+ | sqlalchemy | 1.4.54 → 2.1.3 | ✅ | $0.19 | 1 file changed, 7 insertions(+), 4 deletions(-) |
40
+ | pydantic (6-file app, shared base model) | 1.10.21 → 2.13.5 | ✅ | $0.51 | 4 files changed, 47 insertions(+), 29 deletions(-) |
41
+
42
+ 6/6 fixed for $1.45 total, no test edits. The 6-file case runs with deprecation warnings as errors and includes a silent behavior change (v2 stops coercing numeric SKUs to strings). Rerun: `python bench/run.py` (or `--check` to validate cases for free).
43
+ More cases welcome: add a folder under `bench/cases/`.
44
+
45
+ ## GitHub Action
46
+
47
+ ```yaml
48
+ # .github/workflows/driftfix.yml
49
+ name: driftfix
50
+ on: pull_request
51
+ permissions: { contents: write, pull-requests: write }
52
+ jobs:
53
+ fix:
54
+ if: github.actor == 'dependabot[bot]'
55
+ runs-on: ubuntu-latest
56
+ steps:
57
+ - uses: actions/checkout@v4
58
+ with: { ref: "${{ github.head_ref }}" }
59
+ - uses: Gthejesraj/driftfix@main
60
+ with:
61
+ anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
62
+ setup: pip install -r requirements.txt
63
+ test: pytest -q
64
+ ```
65
+
66
+ **Important:** workflows triggered by Dependabot can't read your normal repo
67
+ secrets. Add `ANTHROPIC_API_KEY` under **Settings → Secrets → Dependabot**.
68
+
69
+ Commits pushed with the default `GITHUB_TOKEN` don't re-trigger CI. Use a PAT
70
+ or GitHub App token in `actions/checkout` if you want CI to rerun on the fix.
71
+
72
+ ## CLI
73
+
74
+ ```bash
75
+ pip install driftfix
76
+ # after upgrading a dependency and seeing tests fail:
77
+ driftfix fix --package openai --from 0.28 --to 1.40 --test "pytest -q"
78
+ ```
79
+
80
+ | Flag | Default | |
81
+ |---|---|---|
82
+ | `--test` | `pytest -q` | any command; exit 0 = green |
83
+ | `--model` | `claude-opus-5` | |
84
+ | `--budget` | `5` | max USD per run |
85
+ | `--max-turns` | `50` | |
86
+ | `--summary FILE` | | write the markdown report |
87
+
88
+ ## Guardrails
89
+
90
+ - Your tests decide. driftfix reruns them after the agent finishes; the agent's word doesn't count.
91
+ - Edits to dependency files (pinning back) are rejected.
92
+ - Requires a clean git tree, so every change is a reviewable diff.
93
+ - Never merges anything. The PR is still yours to review.
94
+ - The agent runs with full tool access inside the checkout. Run it in CI or a throwaway clone.
95
+
96
+ ## Roadmap
97
+
98
+ - More benchmark cases (pandas 2, Django, langchain), harder multi-file repos
99
+ - npm / Renovate support
100
+ - Provider mode: packages ship migration notes that driftfix reads
101
+
102
+ ## License
103
+
104
+ MIT
@@ -0,0 +1,7 @@
1
+ driftfix/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ driftfix/cli.py,sha256=HY8x1AI1HTWbhEnhnKx5ZOTYTkN--Y39cLojhcT-_3E,5723
3
+ driftfix-0.1.0.dist-info/METADATA,sha256=EsExwlUUHt-Dkvy6cvC6VQ_htJfAMhDnYzhQyWpLm_Y,3969
4
+ driftfix-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
5
+ driftfix-0.1.0.dist-info/entry_points.txt,sha256=Gn3xBJyokXKDUsya24AbEE8W_6Gj-Vt00IRbvNeR4tU,47
6
+ driftfix-0.1.0.dist-info/licenses/LICENSE,sha256=uewuVeXSAUsHUGdfCwBJow1jblK9GtiJwMRdVBSvXzQ,1078
7
+ driftfix-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ driftfix = driftfix.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 driftfix contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.