Metadata-Version: 2.5
Name: patch-linter
Version: 1.0.0
Summary: Static analyzer that forbids unittest.mock.patch and pytest monkeypatch in tests
Project-URL: Homepage, https://github.com/pavelmaksimov/patch-linter
Project-URL: Bug Tracker, https://github.com/pavelmaksimov/patch-linter/issues
Author-email: Pavel Maksimov <vur21@yandex.ru>
License-Expression: MIT
License-File: LICENSE
Requires-Python: >=3.11
Provides-Extra: flake8
Requires-Dist: flake8>=7.2.0; extra == 'flake8'
Description-Content-Type: text/markdown

# Patch Linter

Static analyzer that forbids **`unittest.mock.patch`** and **pytest `monkeypatch`**
mutations in tests. Prefer real dependency injection over patching.

## Why

Patching reaches into other modules and couples tests to implementation details.
Refactoring becomes harder, and failures become brittle. Inject dependencies
(fakes, stubs, or test doubles passed as arguments) instead.

## What is reported

| Pattern | Example |
|---|---|
| `unittest.mock.patch` | `@patch(...)`, `with patch(...)` |
| Import aliases | `from unittest.mock import patch as p` → `p(...)` |
| Via module | `mock.patch(...)`, `unittest.mock.patch(...)` |
| Patch helpers | `patch.object` / `patch.dict` / `patch.multiple` / `patch.stopall` |
| pytest monkeypatch | `monkeypatch.setattr(...)`, `setenv`, `delattr`, … |

Not reported (by design): `mocker` (pytest-mock), bare `Mock` / `MagicMock`,
freezegun, your own functions named `patch`, HTTP `client.patch(...)`.

Code: **`PATCH001`**.

## Installation

```bash
pip install patch-linter
# optional flake8 plugin:
pip install "patch-linter[flake8]"
```

## Usage

### CLI

```bash
patch-linter tests/
patch-linter tests/unit tests/test_foo.py
```

Output (clickable in Cursor / VS Code / PyCharm):

```text
/abs/path/to/tests/test_foo.py:12:5: PATCH001 Patch usage in tests: @patch('module.function')
```

Exit code `1` if problems are found.

### Flake8

```bash
flake8 --select=PATCH tests/
```

### Suppress a line

```python
@patch("module.function")  # noqa: PATCH001
def test_legacy(mock_fn):
    ...
```

Bare `# noqa` also suppresses the finding on that line.

## Correct approaches

```python
def process_data(repository):
    return repository.get_all()


def test_process_data():
    class FakeRepo:
        def get_all(self):
            return [1, 2, 3]

    assert process_data(FakeRepo()) == [1, 2, 3]
```

## License

MIT
