Metadata-Version: 2.4
Name: pyfixer-ai
Version: 0.5.4
Summary: The auto-fix button for Python: upload a broken file, get a fixed file.
License: MIT
Project-URL: Homepage, https://github.com/hix-pro/pyfixer
Project-URL: Repository, https://github.com/hix-pro/pyfixer
Keywords: lint,autofix,refactor,code-quality,security
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Quality Assurance
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: fastapi>=0.110
Requires-Dist: uvicorn>=0.27
Requires-Dist: python-multipart>=0.0.9
Requires-Dist: httpx>=0.27
Requires-Dist: ruff>=0.5
Requires-Dist: bandit>=1.7
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
Requires-Dist: mypy>=1.10; extra == "dev"
Requires-Dist: hypothesis>=6.100; extra == "dev"
Dynamic: license-file

# PyFixer

**The auto-fix button for Python — conservative mechanical fixes by default, LLMs only behind behavioral gates.**

Upload a broken file. Get a fixed file. That's it.

```
Upload → Scan → Fix → Verify → Contracts → Done
```

---

## What It Does

PyFixer finds Python problems and fixes the ones it can prove safe.
Anything it cannot verify, it leaves untouched and reports — abstention
is a successful safety outcome, not a failed fix. See [What the numbers
mean](#what-the-numbers-mean) for exactly what is (and isn't) measured.

| Problem | Fix |
|---|---|
| Security holes | Replaces eval, exec, hardcoded passwords, weak crypto |
| Unused imports | Removes them |
| Wrong types | Adds type annotations |
| Bad style | Formats code, fixes line length |
| Missing docs | Adds docstrings |
| Old Python | Modernizes to current syntax |
| Bare excepts | Changes to specific exceptions |
| Print statements | Converts to logging |
| Mutable defaults | Fixes `def foo(x=[])` |
| Builtin shadowing | Fixes `list = x` |
| Type comparison | Fixes `type(x)==type(y)` |
| Performance | Fixes O(n²) loops, string concat |
| Weak random | Flags non-crypto `random` in security contexts (no blind swap: `secrets` returns a different type) |
| Weak hash | Replaces MD5 with SHA256 |
| Logic bugs | Behavioral tests catch wrong operators, crash-on-empty, stubs |

**17+ types of issues. One click to fix.**

---

## How It Works

```
1. Upload .py file
   ↓
2. Scan with ruff + bandit + mypy + custom/semantic checks
   ↓
3. Generate a behavioral test for EVERY function (coverage guarantee)
   ↓
4. AI proposes a fix (13 specialist workers)
      - clean functions are never sent to the LLM
   ↓
5. Post-processing layer applies guaranteed fixes
      - hardcoded secrets are scrubbed from the output
   ↓
6. CONTRACT GATE: probe original vs fixed behavior
      - untouched functions must return identical results
      - perf/logging-only fixes must be value-identical
      - violations -> corrective retry -> verbatim splice
   ↓
7. Verify: syntax check + ruff + mypy + findings count
   ↓
8. You approve or reject
   ↓
9. Done. File is fixed.
```

---

## Features

### 13 Specialist Workers

Each worker is an expert in one thing:

| Worker | What It Fixes |
|---|---|
| Syntax Surgeon | Syntax errors, indentation |
| Security Guard | SQL injection, hardcoded passwords, weak crypto |
| Type Tamer | Missing type annotations, wrong types |
| Style Formatter | Line length, imports, formatting |
| Code Cleaner | Dead code, unused variables |
| Docstring Writer | Missing docstrings, D101-D107 |
| Annotation Expert | Missing annotations, ANN001-ANN204 |
| Modernizer | Old Python syntax, PTH/SIM/RET |
| Performance Optimizer | O(n²) loops, string concatenation |
| Logger | print() → logging conversion |
| Error Handler | Bare excepts, broad exceptions |
| Generalist | Complex issues, multiple codes |
| MyPy Fixer | Type ignore comments, mypy-specific |

### BYOK (Bring Your Own Key)

You bring your own API key. We never store it.

| Provider | Key prefix | Model |
|---|---|---|
| Google Gemini | `AQ.` | gemini-3.7-flash |
| DeepSeek | `sk-` (probed) | deepseek-chat |
| OpenRouter | `sk-or-` | deepseek/deepseek-v4-flash |
| OpenAI | `sk-` | gpt-4o-mini |

#### Server-side key pool (optional)

Tired of pasting a key into the UI? Configure keys on the server and every
client can fix without one:

```bash
# Option A: environment variable (comma or newline separated)
export PYFIXER_KEYS="AQ.Ab8RN6...,AQ.Ab8RN7..."

# Option B: key file, one key per line
$EDITOR data/gemini.keys   # gitignored — never commit this file
```

Keys rotate automatically (N keys = N × 15 requests/min). The pool is only
used when a request carries no `X-Api-Key` header / body key; client keys
always take priority and are never stored, logged, or written to disk.
`GET /api/server-keys` reports `{ "available": true }` without exposing them.

> The server is a **local tool, not a shared service**: no authentication,
> no per-user isolation, no rate limits. Bind it to loopback only (all
> examples use `--host 127.0.0.1`) and do not expose it to a network.
> The server-side key pool exists for your own machines, not for hosting
> other people's fixes.

### LLM control

Every model call goes through a task profile (temperature / token budget /
JSON-mode) so behaviour is tunable in one place — `PROFILES` in
`pyfixer/byok.py`:

| Profile | Used for | Temperature | Max tokens |
|---|---|---|---|
| `fix` | code fixes | 0.0 | 4096 |
| `fix_escalate` | contract-violation retries | 0.0 | 4096 |
| `testgen` | behavioral test generation | 0.0 | 3072 |
| `classify` | finding classification | 0.0 | 512 + JSON mode |
| `probe` | cheap probes | 0.0 | 256 |

Model resolution order: per-call override → `PYFIXER_MODEL` env var →
provider default. The classifier requests provider-side JSON mode (Gemini
`response_mime_type`, OpenAI-compatible `response_format`).

### Verify & Rollback

- **Verify**: Proves the fix works (syntax + ruff + mypy + findings)
- **Rollback**: Undo if you don't like it

### Pattern Learning

PyFixer remembers what works. The more you use it, the better it gets.

---

## Quickstart

The fastest way — install from PyPI (no repo clone needed):

```bash
# Recommended: isolated install via pipx
pipx install pyfixer-ai
pyfixer scan app.py

# Or with pipx unavailable, a virtualenv:
python3 -m venv ~/.venvs/fixer
~/.venvs/fixer/bin/pip install pyfixer-ai
~/.venvs/fixer/bin/pyfixer scan app.py
```

> On Debian/Ubuntu, plain `pip install pyfixer-ai` refuses with
> "externally-managed-environment" — that's your system Python protecting
> itself. Use pipx or a venv as above, never `--break-system-packages`.

From source (for development):

```bash
# Clone
git clone https://github.com/hix-pro/pyfixer.git
cd pyfixer

# Setup
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt

# Run
.venv/bin/uvicorn pyfixer.main:app --host 127.0.0.1 --port 8501
```

Open http://127.0.0.1:8501

> Local use only: always bind `127.0.0.1`, never `0.0.0.0` — the API has
> no login and applies fixes to files on its host. If the install
> location is read-only (system site-packages), point state elsewhere:
> `PYFIXER_ROOT=/path/to/writable` (workspace, SQLite, and learned data
> follow it).

> Note: `python-multipart` is required (pulled in by `requirements.txt`) so the
> upload endpoints that use `Form(...)` work. `ruff` and `bandit` must be
> installed and on `PATH` (the scanner shells out to `python -m ruff` /
> `python -m bandit`). `mypy` is optional — only used when you enable the mypy
> scanner.

---

## CLI

Fix files straight from the terminal — same pipeline, same gates, no server:

```bash
# module form (works from a checkout)
python -m pyfixer fix app.py

# after `pip install -e .` you also get the console script
pyfixer scan app.py            # findings only, no AI call   (exit 1 on bugs; style hidden unless --all)
pyfixer fix app.py             # writes app.fixed.py next to it
pyfixer fix app.py --apply     # overwrite in place
pyfixer fix app.py --diff      # print the patch, write nothing
pyfixer fix src/               # every .py under a folder (summary at end)
pyfixer fix app.py --format json       # machine-readable output
pyfixer fix app.py --worker security   # force a specialist
pyfixer fix app.py --llm              # + LLM fixes (needs API key)
pyfixer fix app.py --llm --fast       # fewer model calls, same gates
pyfixer fix app.py --llm --verify tests/test_app.py --apply   # real-test gate (recommended)
pyfixer fix app.py --llm --apply --unsafe   # write without any gate (you accept regressions)
pyfixer fix app.py --aggressive       # + mechanical ruff upgrades (no key)
pyfixer fix app.py --check            # preview only, write nothing
pyfixer workers                # list all specialists

# key: --key beats $PYFIXER_KEYS beats data/gemini.keys (rotating pool)

# LLM-gated flags: --comprehensive and --reference work ONLY with --llm;
# --verify needs --llm (pytest gate) or --llm-targeted (mypy gate).
# Without them the CLI exits 2 instead of silently ignoring the flag.
# --llm --apply refuses to write without --verify (real-test gate) unless
# --unsafe is passed explicitly.
```

### Deterministic Fixes (no API key needed)

Deterministic mode (the default — no API key needed) applies 50+ pattern-based fixes instantly:

| Code | Fix |
|---|---|
| C408 | `dict(name="test")` → `{'name': 'test'}` |
| C416 | `[x for x in y]` → `list(y)` |
| E711 | `x == None` → `x is None` |
| E712 | `x == True` → `x` |
| E722 | `except:` → `except Exception:` |
| F401 | Remove unused imports |
| F841 | Remove unused variables |
| LOGIC005/009 | Fix inverted comparison signs |
| LOGIC010 | Fix insertion sort off-by-one |
| LOGIC014 | `def f(x=[])` → `def f(x=None)` |
| LOGIC015 | `type(x) == int` → `isinstance(x, int)` |
| LOGIC016 | `for i in range(n)` → `for _ in range(n)` |
| PLR1714 | `x==1 or x==2` → `x in (1, 2)` |
| RET505 | Remove unnecessary `else` after `return` |
| SIM102 | `if x: if y:` → `if x and y:` |
| SIM105 | `try: pass` → `contextlib.suppress()` |
| SIM115 | `open()` → `with open()` |
| SIM116 | `if k in d: return d[k]` → `d.get(k)` |
| SIM118 | `x in d.keys()` → `x in d` |
| SIM210 | `True if x else False` → `bool(x)` |
| SIM212 | `if x: return True else: return False` → `return x` |
| UP030 | `"{}".format(x)` → `f"{x}"` |

**Example:**

```bash
# Quick fix without API key (deterministic is the default)
pyfixer fix buggy.py

# See what would change
pyfixer fix buggy.py --diff

# Apply in place
pyfixer fix buggy.py --apply
```

Exit codes: `0` OK · `1` findings survived the fix · `2` usage/no key.

---

## API

| Method | Endpoint | Description |
|---|---|---|
| GET | `/healthz` | Health check |
| POST | `/api/uploads` | Upload + auto-scan |
| GET | `/api/uploads` | List all files |
| GET | `/api/uploads/{id}` | Get file details |
| POST | `/api/uploads/{id}/fix` | Propose AI fix |
| POST | `/api/uploads/{id}/verify` | Verify fix works |
| POST | `/api/uploads/{id}/rollback` | Undo fix |
| POST | `/api/fixes/{id}/approve` | Approve fix (unverified LLM proposals need `acknowledge_unverified` or `auto_verify`) |
| POST | `/api/fixes/{id}/reject` | Reject fix |
| GET | `/api/uploads/{id}/report` | Download report |
| GET | `/api/workers` | List all workers |
| GET | `/api/audit` | Audit trail |

Approval follows the same policy as the CLI: deterministic, retrieved,
and real-suite-verified fixes apply freely; approving anything else
requires `{"acknowledge_unverified": true}` (the API equivalent of
`--unsafe`) or a prior `POST /api/fixes/{id}/verify`. `auto_verify`
runs a generated test before applying — safer than blind acknowledgment,
but an AI-generated oracle, not an independent ground truth like the
CLI's `--verify` project-suite gate. The browser UI
shows the tier badge and asks for confirmation on unverified proposals.

---

## Scanner

PyFixer uses multiple scanners:

- **ruff** — Fast Python linter (16 rule sets)
- **bandit** — Security scanner
- **mypy** — Type checker
- **AST detector** — 16 LOGIC codes + 10 STYLE codes + 5+ ALGO/PERF codes
- **Custom checks** — 8 additional checks:
  - ERR001: Missing error handling
  - ERR002: Broad exception catching
  - LOG001: print() instead of logging
  - PERF001: Inefficient string concatenation
  - MUT001: Mutable default arguments
  - SHADOW001: Builtin name shadowing
  - TYPE001: type() comparison instead of isinstance()

**Total: 50+ types of issues detected.**

---

## Testing

```bash
PYTHONPATH=. .venv/bin/pytest tests/ -v
```

**1067 tests passing** (incl. opt-in corpus parity suites; see
`docs/BENCHMARKS.md` for the current measured numbers).

### What the numbers mean

Four different claims, kept separate on purpose:

| Metric | Current | What it proves |
|---|---|---|
| Benchmark correctness | deterministic lane **114/115** | curated rule-set repairs, not arbitrary Python |
| Finding reduction | e.g. `19 → 5` on the max-bugs demo | scanner findings resolved per file |
| Behavior preservation | contracts + real-suite gates, fail-closed reverts | fixes don't break what passed before |
| Corpus parity | **7/9** real-project suites (2 pre-existing gaps, documented) | no downstream breakage on real repos |

Unproven or refused fixes stay reported, never silently counted. An
abstention (`ORIGINAL` tier — "could not prove safe, changed nothing") is
the system working, not failing.

### Regression benchmarks (LLM-in-the-loop)

`benchmarks/` contains bug files with human-verified `correct.py` references
and `check.py` auto-checkers. The harness runs each through the REAL fix
pipeline and scores it — including clean **canary functions** that must keep
identical behavior (contract-regression guard).

```bash
# all cases (uses server key pool or PYFIXER_BENCH_KEY)
.venv/bin/python benchmarks/run_bench.py

# one case
.venv/bin/python benchmarks/run_bench.py --only logic_signs
```

Cases: `algorithms`, `strings`, `mixed`, `security_basics`, `crash_edges`,
`logic_signs`, `perf_logging`. A manual GitHub Actions workflow
(`.github/workflows/regression.yml`) runs them on demand with a
`PYFIXER_KEYS` secret.

### Corpus regression (downstream suite parity)

`tests/test_corpus_regression.py` runs nine real projects' test suites against
both the pristine checkout and the deterministic rewrite, asserting identical
pass/fail signatures — the harness that caught the RET505 click corruption,
the ERR003 logging-import breakage, and the F401 `# noqa` side-effect import
deletion. Opt-in via `PYFIXER_CORPUS_DIR`:

```bash
bash scripts/provision_corpus.sh /path/to/pyfixer-corpus   # checkouts + venvs
PYFIXER_CORPUS_DIR=/path/to/pyfixer-corpus PYTHONPATH=. pytest tests/test_corpus_regression.py -m corpus -q
```

Repos: click, httpie, requests, rich, flask, fastapi, attrs (2022-era),
httpx 1.0b0 (2025-era), pandas 2.2.3. Pandas is special: its compiled `_libs`
can't be PYTHONPATH-shadowed, so it runs as a dedicated **two-venv** parity
test (`venv-pandas` vs `venv-pandas-fixed`, both on numpy==1.26.4). A manual +
weekly workflow (`.github/workflows/corpus.yml`) runs the same in CI.

`tests/test_semantic_rules.py` locks the semantic tier (SEM003/SEM004/SEM006)
with a detector mutation oracle plus differential-execution coverage of the
SEM004 fixer; `tests/test_mock_llm_contract.py` locks the SEM003
detector -> per-function-LLM wiring with a stubbed model (no network).

## Code quality

```bash
# Lint + auto-format
.venv/bin/ruff check pyfixer
.venv/bin/ruff format pyfixer

# Static type checking
.venv/bin/mypy pyfixer
```

A GitHub Actions workflow (`.github/workflows/ci.yml`) runs `ruff check`,
`ruff format --check`, `mypy`, and the full pytest suite on every push and
pull request, so regressions are caught before merge.

---

## Tech Stack

- **Backend**: FastAPI + SQLite + Python
- **Scanner**: ruff + bandit + mypy + custom AST checks
- **AI**: OpenAI, OpenRouter, Google, DeepSeek (BYOK)
- **Testing**: pytest + coverage

## Tier Semantics

PyFixer promotes fixes through four tier levels. The goal is honest tier assignment — a function's tier reflects the evidence that validates its rewrite.

| Tier | When it's assigned | Promotion path |
|---|---|---|
| `UNVERIFIED` | Default for any fixed function that has **no oracle** (no `func_test`). LOGIC‑only fixes (ast‑detector findings) always land here, regardless of `verify_mode`. | After the real project suite passes (`--verify` in `cli.py`), tiers are promoted by `_promote_tiers_to_verified` in `cli.py:1168/1276`. |
| `VERIFIED-AI` | A generated `func_test` exists and the rewrite passes `_valid_fix`. The rewrite was validated against the AI‑generated test. | Same real‑suite promotion; this is the interim tier after propose. |
| `VERIFIED` | Earned only after the **real project suite** (`--verify`) runs successfully via `_promote_tiers_to_verified`. No automatic path mint `VERIFIED` at propose time. | Manual / CI gate after suite runs. |
| `RETRIEVED` | Deterministic fixes that match an existing reference (e.g. RETRIEVED from known‑good repo). | Stays RETRIEVED; no further promotion. |

**Honesty invariant:** `verify_mode=True` does **not** mint `VERIFIED`. It only means the user passed `--verify`; a real suite still needs to run afterwards. Promotion to `VERIFIED` belongs to the real‑suite gate in `cli.py`, never `propose()`.

---

## License

Copyright (c) 2026 Aziz. All rights reserved.

---

## Author

**Aziz** — Built PyFixer to fix Python code automatically.
