Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ repos:
- repo: local
hooks:
- id: ast-grep-rule-tests
name: Test monkeypatch rule
name: Test testing policy rules
language: python
entry: ast-grep test --config sgconfig.yml
additional_dependencies: ["ast-grep-cli==0.45.2"]
pass_filenames: false
files: ^(src/python_mock_hooks/rules/|tests/|sgconfig\.yml$)
files: ^(src/python_mock_hooks/(rules|if-rules)/|tests/|sgconfig\.yml$)
9 changes: 9 additions & 0 deletions .pre-commit-hooks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,12 @@
files: ^tests/.*\.py$
require_serial: true
stages: [pre-commit, pre-merge-commit, pre-push, manual]
- id: tests-without-ifs
name: Check for if statements in tests
description: Reject if statements in test bodies; use separate tests or parametrization.
entry: tests-without-ifs
language: python
types: [python]
files: ^tests/.*\.py$
require_serial: true
stages: [pre-commit, pre-merge-commit, pre-push, manual]
31 changes: 23 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.23183666.svg)](https://doi.org/10.5281/zenodo.23183666)
[![Research Software Directory Badge](https://img.shields.io/badge/rsd-00a3e3.svg)](https://research-software-directory.org/software/python-mock-precommit-hook)

A pre-commit / prek hook that restricts mocking in Python tests:
Two independently selectable pre-commit / prek hooks for Python tests.

### `python-mock-hooks`

- **Blocks**:
- `unittest.mock` imports and references.
Expand All @@ -19,10 +21,21 @@ A pre-commit / prek hook that restricts mocking in Python tests:
For HTTP tests, prefer recording requests with
[pytest-recording](https://github.com/kiwicom/pytest-recording).

### `tests-without-ifs`

Blocks `if` / `elif` / `else` branching statements inside `test_*` functions
and methods, including async tests. Use separate tests or
`pytest.mark.parametrize` for each case instead.

Permits `if` statements in fixtures, helpers (including nested helpers), and
at module or class scope, as well as conditional expressions
(`a if condition else b`) and comprehension filters. This hook does not
restrict mocking; `python-mock-hooks` does not restrict branching.

Check failures explain the policy to developers and LLM coding agents.

Ruff [does not support custom lint plugins](https://docs.astral.sh/ruff/faq/#can-i-write-my-own-linter-plugins-for-ruff),
so this hook adds these testing policies as a separate check alongside Ruff.
so these hooks add testing policies as separate checks alongside Ruff.

## Usage

Expand All @@ -35,24 +48,26 @@ For `.pre-commit-config.yaml` (pre-commit or prek):
```yaml
repos:
- repo: https://github.com/i-VRESSE/python-mock-hooks
rev: v0.2.0
rev: v0.3.0
hooks:
- id: python-mock-hooks
- id: tests-without-ifs
```

For `prek.toml` (prek):

```toml
[[repos]]
repo = "https://github.com/i-VRESSE/python-mock-hooks"
rev = "v0.2.0"
hooks = [{ id = "python-mock-hooks" }]
rev = "v0.3.0"
hooks = [{ id = "python-mock-hooks" }, { id = "tests-without-ifs" }]
```

Run `prek run python-mock-hooks --all-files` (or use `pre-commit` instead of `prek`).
Enable either hook or both. Run `prek run --all-files` to run all enabled hooks
(or use `pre-commit` instead of `prek`).

Checks Python files under `tests/` by default. For another layout, set
`files` on the hook:
Both hooks check Python files under `tests/` by default. For another layout,
set `files` on each enabled hook:

- YAML: `files: ^(tests|test)/.*\.py$`
- TOML: `files = '^(tests|test)/.*\.py$'`
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ dependencies = ["ast-grep-cli==0.45.2"]

[project.scripts]
python-mock-hooks = "python_mock_hooks:main"
tests-without-ifs = "python_mock_hooks:tests_without_ifs"

[tool.hatch.build.targets.wheel]
packages = ["src/python_mock_hooks"]
Expand Down
89 changes: 89 additions & 0 deletions rename.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Naming options

The repository contains two independently selectable Python test policies:

- Restrict `unittest.mock`, `mocker.patch`, and most `monkeypatch` methods.
- Reject `if` statements inside `test_*` functions and methods.

Compatibility is not a requirement. The options below assume a clean rename,
without legacy hook IDs or command aliases.

## Examples from established hooks

Existing projects use several naming patterns rather than one uniform convention:

- Actions: `check-ast`, `detect-private-key`, and `check-builtin-literals` in
[pre-commit-hooks](https://github.com/pre-commit/pre-commit-hooks/blob/main/.pre-commit-hooks.yaml).
- Prohibitions with a language prefix: `python-no-eval` and
`python-no-log-warn` in
[pygrep-hooks](https://github.com/pre-commit/pygrep-hooks/blob/main/.pre-commit-hooks.yaml).
- Tool and action: `ruff-check` and `ruff-format` in
[ruff-pre-commit](https://github.com/astral-sh/ruff-pre-commit/blob/main/.pre-commit-hooks.yaml).

These examples support short, descriptive hook IDs with hyphens. A collection's
repository name can describe its overall purpose while each hook ID describes
one policy.

## Repository names

| Name | Pros | Cons |
| --- | --- | --- |
| **`python-test-hooks`** | Clearly identifies the language and purpose; accommodates additional test policies. | Broad; does not emphasize that the rules are opinionated. |
| `python-test-policy-hooks` | Explicitly describes selectable testing policies. | Longer and less convenient to type. |
| `python-test-pre-commit` | Makes the integration immediately obvious, following the `ruff-pre-commit` pattern. | Less natural if the commands also become standalone lint tools. |
| `python-test-lint` | Concise; describes static checks and suits standalone use. | Does not advertise the pre-commit integration. |
| `python-mock-hooks` | Short and accurately describes the original mocking policy. | Does not describe the new conditional-statement hook or future unrelated policies. |

## Mocking hook alternatives to `python-mock-hooks`

The policy permits `mocker.Mock`, `mocker.spy`, and several `monkeypatch` methods.
Its name should therefore describe restrictions rather than promise a complete
ban on mocks.

| Hook ID | Pros | Cons |
| --- | --- | --- |
| **`check-test-mocking`** | Uses the established `check-*` pattern; covers all three mocking APIs without claiming a total ban. | The exact restrictions require a description. |
| `restrict-test-mocking` | Clearly communicates selective restrictions and test scope. | `restrict-*` is less familiar than the patterns in the examples above. |
| `check-mocking-policy` | Accurately communicates an opinionated policy with exceptions. | Omits test scope and is somewhat abstract. |
| `python-check-test-mocking` | Explicit language and test scope; follows the language-prefix pattern used by pygrep-hooks. | Longer; Python is already present in the recommended repository name. |
| `no-patching-in-tests` | Directly communicates the main discouraged behavior. | Overstates the policy because environment and working-directory monkeypatching are allowed; also understates the ban on `unittest.mock` imports. |
| `tests-with-restricted-mocking` | Reads naturally and pairs with `tests-without-ifs`. | Long and less consistent with common action-based hook IDs. |

Avoid `no-mocks` and `tests-without-mocks`: both imply that all mocking is
forbidden, which does not match the implementation.

## Conditional-statement hook names

| Hook ID | Pros | Cons |
| --- | --- | --- |
| **`no-if-in-tests`** | Direct; follows the prohibition pattern; matches the existing rule ID. | Its description should clarify that conditional expressions remain allowed. |
| `python-no-if-in-tests` | Explicit language scope; follows pygrep-hooks naming. | Longer and repeats the language in the recommended repository name. |
| `tests-without-ifs` | Readable and already used by the new hook. | Less consistent with the action and prohibition patterns above; `ifs` is informal. |
| `check-test-if-statements` | Uses `check-*` and precisely identifies the syntax being checked. | Does not immediately communicate that every matching statement is forbidden. |
| `check-test-conditionals` | Concise and uses the established `check-*` pattern. | Suggests coverage of conditional expressions and comprehension filters, which are allowed. |
| `no-test-branching` | Communicates the policy's motivation. | Overstates coverage: `match`, conditional expressions, and other control flow remain allowed. |

## Suggested combinations

| Style | Repository | Mocking hook | Conditional-statement hook | Tradeoff |
| --- | --- | --- | --- | --- |
| **Recommended** | `python-test-hooks` | `check-test-mocking` | `no-if-in-tests` | Short, descriptive names; each hook uses the wording that best fits its actual policy. |
| Explicit language | `python-test-hooks` | `python-check-test-mocking` | `python-no-if-in-tests` | Hook IDs make sense outside the repository context, at the cost of length. |
| Consistent actions | `python-test-policy-hooks` | `check-test-mocking` | `check-test-if-statements` | Both IDs follow `check-*`; the prohibition is expressed in the hook description. |
| Natural phrasing | `python-test-hooks` | `tests-with-restricted-mocking` | `tests-without-ifs` | Both names describe the desired tests; the mocking ID is lengthy. |

With the recommended names, the consumer configuration would be:

```yaml
repos:
- repo: https://github.com/i-VRESSE/python-test-hooks
rev: v0.3.0
hooks:
- id: check-test-mocking
- id: no-if-in-tests
```

These are proposals; repository, package, command, and hook names have not been
changed. Once a combination is selected, the rename should update the GitHub
repository, package metadata and import path, console commands, hook manifest,
documentation, and integration tests together.
1 change: 1 addition & 0 deletions sgconfig.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
ruleDirs:
- src/python_mock_hooks/rules
- src/python_mock_hooks/if-rules
testConfigs:
- testDir: tests
14 changes: 12 additions & 2 deletions src/python_mock_hooks/__init__.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
"""Run the bundled monkeypatch policy against explicit filenames."""
"""Run the bundled testing policies against explicit filenames."""

import subprocess
import sys
from pathlib import Path


def main() -> int:
"""Check mocking policies."""
return _scan("sgconfig.yml")


def tests_without_ifs() -> int:
"""Check for if statements in test bodies."""
return _scan("ifs-sgconfig.yml")


def _scan(config_name: str) -> int:
"""Forward filenames to ast-grep and preserve its exit status."""
filenames = sys.argv[1:]
if not filenames:
return 0

config = Path(__file__).with_name("sgconfig.yml")
config = Path(__file__).with_name(config_name)
return subprocess.run(
["ast-grep", "scan", "--config", str(config), "--", *filenames],
check=False,
Expand Down
15 changes: 15 additions & 0 deletions src/python_mock_hooks/if-rules/no-if-in-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
id: no-if-in-tests
language: Python
severity: error
message: Do not use if statements in test bodies; use separate tests or pytest.mark.parametrize for each case
rule:
kind: if_statement
inside:
kind: function_definition
has:
field: name
regex: '^test_'
stopBy:
any:
- kind: function_definition
- kind: class_definition
2 changes: 2 additions & 0 deletions src/python_mock_hooks/ifs-sgconfig.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ruleDirs:
- if-rules
Loading
Loading