# depwhy

Explains Python dependency conflicts in plain English and suggests ranked fixes.

## The Problem

When `pip`, `uv`, or `poetry` fails to resolve dependencies, the error output is verbose, cryptic, and non-actionable. You're left manually cross-referencing PyPI to figure out which packages conflict and why. `depwhy` reads your requirements, finds every conflict, and tells you exactly what to change — with a ready-to-run `pip install` command.

## Install

```bash
pip install depwhy
```

Or with `uv`:

```bash
uv pip install depwhy
```

Requires Python 3.10+.

## CLI Usage

```bash
# Analyze a requirements.txt file
depwhy requirements.txt

# Analyze a pyproject.toml file
depwhy pyproject.toml

# Analyze packages installed in the current virtualenv
depwhy --env

# Output machine-readable JSON
depwhy requirements.txt --json

# Exit code only (0 = clean, 1 = conflicts) — useful in CI
depwhy requirements.txt --quiet

# Use cached metadata only (no network calls)
depwhy requirements.txt --offline

# Override target Python version for environment marker evaluation
depwhy requirements.txt --python-version 3.11
```

### CLI Flags

| Flag               | Description                                     |
|--------------------|-------------------------------------------------|
| `--env`            | Analyze installed packages in the current venv  |
| `--json`           | Output machine-readable JSON to stdout          |
| `--quiet`          | No output; exit code only                       |
| `--offline`        | Use disk cache only — no network calls          |
| `--python-version` | Override Python version for marker evaluation   |
| `--no-color`       | Disable colored terminal output                 |
| `--version`        | Print version and exit                          |

### Exit Codes

| Code | Meaning          |
|------|------------------|
| 0    | No conflicts     |
| 1    | Conflicts found  |
| 2    | Error            |

## Example Output

```
$ depwhy requirements.txt

2 conflicts found

urllib3
  Problem: requests==2.28.0 requires urllib3<1.27, but you have urllib3==2.0.0 pinned
  Solution: Remove your pin — use urllib3==1.26.18 (latest compatible)
    → pip install urllib3==1.26.18
  Alternative: Upgrade requests to >=2.29.0 (loosens urllib3 upper bound)
    → pip install requests==2.29.0

certifi
  Problem: cryptography==41.0.0 requires certifi<2023.0, but pip requires certifi>=2017.4.17
  Solution: Downgrade certifi to satisfy both constraints
    → pip install certifi==2022.12.7
  Alternative: Upgrade cryptography to >=42.0.0 (drops certifi upper bound)
    → pip install cryptography==42.0.0
```

## Library API

```python
import depwhy

report = depwhy.analyze("requirements.txt")

if report.has_conflicts:
    for conflict in report.conflicts:
        print(conflict.package)
        print(f"  Problem:     {conflict.problem}")
        print(f"  Solution:    {conflict.solution.description}")
        print(f"               {conflict.solution.pip_command}")
        if conflict.alternative:
            print(f"  Alternative: {conflict.alternative.description}")
            print(f"               {conflict.alternative.pip_command}")
else:
    print(f"No conflicts found ({report.analyzed_packages} packages checked)")
```

The `analyze()` function accepts a path to `requirements.txt` or `pyproject.toml`, a list of requirement strings, or a `pathlib.Path` object:

```python
# From a list of requirement strings
report = depwhy.analyze(["django>=4.0", "celery==5.2", "kombu==5.3"])

# With Python version and platform for environment marker evaluation
report = depwhy.analyze("requirements.txt", python_version="3.11", platform="linux")

# Offline mode (uses disk cache only — no network calls)
report = depwhy.analyze("requirements.txt", offline=True)
```

### Return Types

`analyze()` returns a `ConflictReport`:

| Field               | Type             | Description                              |
|---------------------|------------------|------------------------------------------|
| `has_conflicts`     | `bool`           | Whether any conflicts were found         |
| `conflicts`         | `list[Conflict]` | All detected conflicts                   |
| `warnings`          | `list[str]`      | Narrowed-but-valid ranges (soft issues)  |
| `analyzed_packages` | `int`            | Total packages examined                  |

Each `Conflict` contains:

| Field          | Type                    | Description                                 |
|----------------|-------------------------|---------------------------------------------|
| `package`      | `str`                   | The package that cannot be resolved         |
| `problem`      | `str`                   | Plain-English one-liner describing the conflict |
| `constraints`  | `list[Constraint]`      | All incoming version constraints            |
| `solution`     | `FixCandidate`          | Recommended fix                             |
| `alternative`  | `FixCandidate \| None`  | Second-best fix                             |

Each `FixCandidate` contains:

| Field            | Type   | Description                                      |
|------------------|--------|--------------------------------------------------|
| `description`    | `str`  | Human-readable explanation of the fix            |
| `pip_command`    | `str`  | Copy-pasteable `pip install` command             |
| `is_alternative` | `bool` | `False` for the top fix, `True` for the fallback |

## How It Works

`depwhy` follows a six-stage pipeline: **parse → fetch → graph → detect → rank → explain**.

1. **Parse** — reads your `requirements.txt` or `pyproject.toml` to collect every dependency and version pin.
2. **Fetch** — retrieves dependency metadata from PyPI in parallel using async HTTP, caching results locally (`~/.cache/depwhy/`) so repeat runs are instant.
3. **Graph** — builds a directed constraint graph (via `networkx`) connecting every package to the packages that require it and the version ranges they accept. Environment markers are evaluated so platform-specific dependencies are handled correctly.
4. **Detect** — computes the intersection of all version constraints for each package using PEP 440 specifier algebra. An empty intersection means no single version can satisfy every requirement (hard conflict). A narrowed intersection triggers a soft warning.
5. **Rank** — evaluates three fix strategies for each conflict — loosen a user pin, upgrade a depender, or downgrade a depender — and selects the best fix (fewest changes, smallest version delta, prefer newer) plus a fallback alternative.
6. **Explain** — generates a plain-English problem description and renders the output to the terminal with color-coded labels, or as structured JSON.

### Architecture

```
src/depwhy/
├── _analyze.py          # Core analyze() orchestrator
├── cli.py               # Typer CLI entrypoint
├── parsers/
│   ├── requirements.py  # requirements.txt parser
│   └── pyproject.py     # pyproject.toml parser
├── fetcher/
│   ├── pypi.py          # Async PyPI metadata client (httpx + semaphore)
│   └── cache.py         # Disk cache (SHA-256 keyed, TTL via mtime)
├── graph/
│   ├── builder.py       # Constraint graph construction (networkx.DiGraph)
│   └── models.py        # Frozen dataclasses: Constraint, Conflict, etc.
├── solver/
│   ├── detector.py      # Conflict detection (SpecifierSet intersection)
│   └── ranker.py        # Fix candidate ranking (3 strategies)
└── explain/
    ├── generator.py     # Template-based explanation builder
    └── formatter.py     # Rich terminal + JSON formatter
```

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, code quality standards, and the pull request process.

## License

MIT — see [LICENSE](LICENSE).
