Skip to content

Commit 9bc0d59

Browse files
committed
feat: initial implementation
1 parent 5f2b1cf commit 9bc0d59

12 files changed

Lines changed: 890 additions & 34 deletions

File tree

.github/workflows/publish.yml

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
name: Publish to PyPI
2+
3+
on:
4+
push:
5+
tags:
6+
- "v*"
7+
8+
permissions:
9+
id-token: write
10+
11+
jobs:
12+
test:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v4
16+
with:
17+
fetch-depth: 0
18+
- uses: actions/setup-python@v5
19+
with:
20+
python-version: "3.13"
21+
- name: Install dependencies
22+
run: pip install tox ruff mypy pyright pytest
23+
- name: Run tests
24+
run: tox -e py
25+
- name: Ruff check
26+
run: ruff check src tests
27+
- name: Ruff format
28+
run: ruff format --check src tests
29+
- name: mypy
30+
run: mypy src
31+
- name: pyright
32+
run: pyright src
33+
34+
publish:
35+
needs: test
36+
runs-on: ubuntu-latest
37+
environment: pypi
38+
39+
steps:
40+
- uses: actions/checkout@v4
41+
with:
42+
fetch-depth: 0
43+
44+
- uses: actions/setup-python@v5
45+
with:
46+
python-version: "3.13"
47+
48+
- name: Install build tools
49+
run: pip install build
50+
51+
- name: Build package
52+
run: python -m build
53+
54+
- name: Publish to PyPI
55+
uses: pypa/gh-action-pypi-publish@release/v1

.github/workflows/test.yml

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
test:
11+
runs-on: ${{ matrix.os }}
12+
strategy:
13+
fail-fast: false
14+
matrix:
15+
python-version: ["3.10", "3.11", "3.12", "3.13"]
16+
os: [ubuntu-latest, windows-latest, macos-latest]
17+
18+
steps:
19+
- uses: actions/checkout@v4
20+
with:
21+
fetch-depth: 0
22+
23+
- name: Set up Python ${{ matrix.python-version }}
24+
uses: actions/setup-python@v5
25+
with:
26+
python-version: ${{ matrix.python-version }}
27+
28+
- name: Install tox
29+
run: pip install tox
30+
31+
- name: Run tests
32+
run: tox -e py
33+
34+
lint:
35+
runs-on: ubuntu-latest
36+
steps:
37+
- uses: actions/checkout@v4
38+
- uses: actions/setup-python@v5
39+
with:
40+
python-version: "3.13"
41+
- run: pip install ruff
42+
- name: Ruff check
43+
run: ruff check src tests
44+
- name: Ruff format
45+
run: ruff format --check src tests
46+
47+
type-check:
48+
runs-on: ubuntu-latest
49+
steps:
50+
- uses: actions/checkout@v4
51+
with:
52+
fetch-depth: 0
53+
- uses: actions/setup-python@v5
54+
with:
55+
python-version: "3.13"
56+
- name: Install dependencies
57+
run: pip install mypy pyright pytest
58+
- name: mypy
59+
run: mypy src
60+
- name: pyright
61+
run: pyright src

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,13 @@ env/
2727

2828
# Type checking
2929
.mypy_cache/
30+
.pyright/
31+
32+
# Coverage
33+
.coverage
34+
.coverage.*
35+
htmlcov/
36+
coverage.xml
37+
38+
# Setuptools SCM
39+
src/pytest_do_not_mock/_version.py

.pre-commit-config.yaml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
repos:
2+
- repo: https://github.com/pre-commit/pre-commit-hooks
3+
rev: v4.6.0
4+
hooks:
5+
- id: trailing-whitespace
6+
- id: end-of-file-fixer
7+
- id: check-yaml
8+
- id: check-added-large-files
9+
- id: check-merge-conflict
10+
- id: debug-statements
11+
- id: check-toml
12+
13+
- repo: https://github.com/astral-sh/ruff-pre-commit
14+
rev: v0.4.8
15+
hooks:
16+
- id: ruff
17+
args: [--fix]
18+
- id: ruff-format
19+
20+
- repo: local
21+
hooks:
22+
- id: mypy
23+
name: mypy
24+
entry: mypy src
25+
language: system
26+
types: [python]
27+
files: ^src/
28+
pass_filenames: false
29+
30+
- id: pyright
31+
name: pyright
32+
entry: pyright src
33+
language: system
34+
types: [python]
35+
files: ^src/
36+
pass_filenames: false

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Changelog
2+
3+
## 0.1.0 (Unreleased)
4+
5+
- Initial release
6+
- `@pytest.do_not_mock` decorator to block all mocking in a test
7+
- `@pytest.do_not_mock(func1, func2)` to protect specific functions
8+
- Support for function objects and string module paths
9+
- Detection of `unittest.mock.patch` (decorator, context manager, start/stop)
10+
- Detection of `Mock`, `MagicMock`, `AsyncMock`, `create_autospec`
11+
- Python 3.10-3.13 support

README.md

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,88 @@
11
# pytest-do-not-mock
22

3-
A pytest plugin that prevents mocking of specific classes and functions.
3+
Pytest plugin to prevent mocking of critical functions in tests.
44

55
## Installation
66

77
```bash
88
pip install pytest-do-not-mock
99
```
1010

11+
The plugin activates automatically once installed — no configuration needed.
12+
13+
## Usage
14+
15+
### Block all mocking in a test
16+
17+
Use `@pytest.do_not_mock` with no arguments to prevent any mocking:
18+
19+
```python
20+
import pytest
21+
22+
@pytest.do_not_mock
23+
def test_payment_integration():
24+
"""This test must use real implementations, no mocks allowed."""
25+
result = process_payment(100.0)
26+
assert result is True
27+
```
28+
29+
Any attempt to use `Mock()`, `MagicMock()`, `patch()`, or similar will raise `DoNotMockError`.
30+
31+
### Protect specific functions
32+
33+
Pass functions (or string paths) to only block mocking of those targets:
34+
35+
```python
36+
from myapp import process_payment, send_email
37+
38+
@pytest.do_not_mock(process_payment)
39+
def test_selective():
40+
"""process_payment cannot be mocked, but other functions can."""
41+
with patch("myapp.send_email"): # this is fine
42+
result = process_payment(100.0)
43+
assert result is True
44+
```
45+
46+
Multiple functions and string paths are supported:
47+
48+
```python
49+
@pytest.do_not_mock(process_payment, validate_user)
50+
def test_multiple():
51+
...
52+
53+
@pytest.do_not_mock("myapp.payments.charge")
54+
def test_string_path():
55+
...
56+
```
57+
58+
### What gets blocked
59+
60+
**No-args mode** (`@pytest.do_not_mock`):
61+
- `Mock()`, `MagicMock()`, `AsyncMock()`
62+
- `patch()` as decorator, context manager, or `start()`/`stop()`
63+
- `patch.object()`, `patch.dict()`
64+
- `create_autospec()`
65+
66+
**Targeted mode** (`@pytest.do_not_mock(func)`):
67+
- `patch()` targeting the protected function
68+
- `patch.object()` targeting the protected function
69+
- Other mocking is allowed
70+
71+
## Testing
72+
73+
```bash
74+
tox
75+
```
76+
77+
Runs tests across Python 3.10–3.13, plus ruff linting and type checking (mypy + pyright).
78+
79+
```bash
80+
tox -e py313 # single Python version
81+
tox -e linting # ruff check + format
82+
tox -e typing # mypy + pyright
83+
pytest tests/ -v # run tests directly
84+
```
85+
1186
## Development
1287

1388
```bash
@@ -16,5 +91,19 @@ cd pytest-do-not-mock
1691
python3 -m venv .venv
1792
source .venv/bin/activate
1893
pip install -e ".[dev]"
19-
pytest
2094
```
95+
96+
### Project structure
97+
98+
```
99+
src/pytest_do_not_mock/
100+
├── __init__.py # Public API: do_not_mock, DoNotMockError
101+
└── plugin.py # Decorator + pytest hooks
102+
103+
tests/
104+
└── test_do_not_mock.py
105+
```
106+
107+
## License
108+
109+
MIT

0 commit comments

Comments
 (0)