pytest-mockrot 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. pytest_mockrot-0.1.0/.github/ISSUE_TEMPLATE/bug_report.yml +53 -0
  2. pytest_mockrot-0.1.0/.github/ISSUE_TEMPLATE/config.yml +8 -0
  3. pytest_mockrot-0.1.0/.github/ISSUE_TEMPLATE/feature_request.yml +33 -0
  4. pytest_mockrot-0.1.0/.github/PULL_REQUEST_TEMPLATE.md +23 -0
  5. pytest_mockrot-0.1.0/.github/workflows/ci.yml +47 -0
  6. pytest_mockrot-0.1.0/.github/workflows/publish.yml +33 -0
  7. pytest_mockrot-0.1.0/.gitignore +51 -0
  8. pytest_mockrot-0.1.0/AGENTS.md +394 -0
  9. pytest_mockrot-0.1.0/CHANGELOG.md +21 -0
  10. pytest_mockrot-0.1.0/CLAUDE.md +74 -0
  11. pytest_mockrot-0.1.0/CODE_OF_CONDUCT.md +122 -0
  12. pytest_mockrot-0.1.0/CONTRIBUTING.md +116 -0
  13. pytest_mockrot-0.1.0/LICENSE +21 -0
  14. pytest_mockrot-0.1.0/PKG-INFO +261 -0
  15. pytest_mockrot-0.1.0/README.md +226 -0
  16. pytest_mockrot-0.1.0/SECURITY.md +29 -0
  17. pytest_mockrot-0.1.0/cahierdecharge.md +463 -0
  18. pytest_mockrot-0.1.0/pyproject.toml +134 -0
  19. pytest_mockrot-0.1.0/src/mockrot/__init__.py +7 -0
  20. pytest_mockrot-0.1.0/src/mockrot/cli.py +23 -0
  21. pytest_mockrot-0.1.0/src/mockrot/errors.py +19 -0
  22. pytest_mockrot-0.1.0/src/mockrot/extractor.py +180 -0
  23. pytest_mockrot-0.1.0/src/mockrot/interceptor.py +418 -0
  24. pytest_mockrot-0.1.0/src/mockrot/models.py +66 -0
  25. pytest_mockrot-0.1.0/src/mockrot/plugin.py +186 -0
  26. pytest_mockrot-0.1.0/src/mockrot/py.typed +1 -0
  27. pytest_mockrot-0.1.0/src/mockrot/reporter.py +164 -0
  28. pytest_mockrot-0.1.0/src/mockrot/validator.py +420 -0
  29. pytest_mockrot-0.1.0/tests/conftest.py +5 -0
  30. pytest_mockrot-0.1.0/tests/integration/test_async_validation_pytester.py +218 -0
  31. pytest_mockrot-0.1.0/tests/integration/test_interceptor_pytester.py +60 -0
  32. pytest_mockrot-0.1.0/tests/integration/test_plugin_registration.py +49 -0
  33. pytest_mockrot-0.1.0/tests/integration/test_reporter_and_exemption_pytester.py +128 -0
  34. pytest_mockrot-0.1.0/tests/integration/test_return_type_validation_pytester.py +173 -0
  35. pytest_mockrot-0.1.0/tests/integration/test_side_effect_pytester.py +214 -0
  36. pytest_mockrot-0.1.0/tests/integration/test_signature_validation_pytester.py +196 -0
  37. pytest_mockrot-0.1.0/tests/integration/test_zombie_fields_pytester.py +217 -0
  38. pytest_mockrot-0.1.0/tests/unit/test_cli.py +25 -0
  39. pytest_mockrot-0.1.0/tests/unit/test_extractor.py +202 -0
  40. pytest_mockrot-0.1.0/tests/unit/test_interceptor.py +362 -0
  41. pytest_mockrot-0.1.0/tests/unit/test_plugin.py +154 -0
  42. pytest_mockrot-0.1.0/tests/unit/test_reporter.py +195 -0
  43. pytest_mockrot-0.1.0/tests/unit/test_smoke.py +42 -0
  44. pytest_mockrot-0.1.0/tests/unit/test_validator.py +1202 -0
  45. pytest_mockrot-0.1.0/uv.lock +783 -0
@@ -0,0 +1,53 @@
1
+ name: Bug Report
2
+ description: Create a report to help us reproduce and fix a bug in Mockrot
3
+ title: "fix: "
4
+ labels: ["bug"]
5
+ body:
6
+ - type: markdown
7
+ attributes:
8
+ value: |
9
+ Thanks for taking the time to report an issue! Please check if the issue has already been reported before submitting.
10
+
11
+ - type: textarea
12
+ id: description
13
+ attributes:
14
+ label: Bug Description
15
+ description: A clear and concise description of what the bug is.
16
+ validations:
17
+ required: true
18
+
19
+ - type: textarea
20
+ id: reproduction
21
+ attributes:
22
+ label: Minimal Reproducible Example
23
+ description: |
24
+ Please provide a short snippet of code or minimal test case reproducing the issue.
25
+ placeholder: |
26
+ def test_example():
27
+ with patch("..."):
28
+ ...
29
+ validations:
30
+ required: true
31
+
32
+ - type: textarea
33
+ id: environment
34
+ attributes:
35
+ label: Environment Info
36
+ description: |
37
+ OS version, Python version, pytest version, and pydantic version.
38
+ placeholder: |
39
+ - OS: macOS / Ubuntu / Windows
40
+ - Python: 3.12.2
41
+ - pytest: 8.3.0
42
+ - pydantic: 2.8.2
43
+ - pytest-mockrot: 0.1.0
44
+ validations:
45
+ required: true
46
+
47
+ - type: textarea
48
+ id: expected
49
+ attributes:
50
+ label: Expected Behavior
51
+ description: What did you expect to happen?
52
+ validations:
53
+ required: true
@@ -0,0 +1,8 @@
1
+ blank_issues_enabled: false
2
+ contact_links:
3
+ - name: Questions & Community Discussions
4
+ url: https://github.com/ademboukabes/Mockrot/discussions
5
+ about: Ask questions and discuss features with the community.
6
+ - name: Security Vulnerability Reports
7
+ url: https://github.com/ademboukabes/Mockrot/blob/main/SECURITY.md
8
+ about: Privately report security concerns via email.
@@ -0,0 +1,33 @@
1
+ name: Feature Request
2
+ description: Suggest an idea or enhancement for Mockrot
3
+ title: "feat: "
4
+ labels: ["enhancement"]
5
+ body:
6
+ - type: markdown
7
+ attributes:
8
+ value: |
9
+ Thanks for helping improve Mockrot! Please describe your proposed feature or enhancement.
10
+
11
+ - type: textarea
12
+ id: problem
13
+ attributes:
14
+ label: Problem or Motivation
15
+ description: Is your feature request related to a specific problem or limitation?
16
+ validations:
17
+ required: true
18
+
19
+ - type: textarea
20
+ id: solution
21
+ attributes:
22
+ label: Proposed Solution
23
+ description: A clear and concise description of what you want to happen.
24
+ validations:
25
+ required: true
26
+
27
+ - type: textarea
28
+ id: alternatives
29
+ attributes:
30
+ label: Alternatives Considered
31
+ description: A description of any alternative solutions or workarounds you have considered.
32
+ validations:
33
+ required: false
@@ -0,0 +1,23 @@
1
+ ## Description
2
+
3
+ Please provide a brief summary of the changes introduced in this pull request and why they are needed.
4
+
5
+ Fixes #(issue number)
6
+
7
+ ## Type of Change
8
+
9
+ - [ ] Bug fix (`fix:`)
10
+ - [ ] New feature (`feat:`)
11
+ - [ ] Performance improvement (`perf:`)
12
+ - [ ] Refactor (`refactor:`)
13
+ - [ ] Documentation update (`docs:`)
14
+ - [ ] CI or build tooling (`ci:`, `build:`)
15
+
16
+ ## Checklist
17
+
18
+ - [ ] My code follows the coding style and guidelines in `CONTRIBUTING.md`.
19
+ - [ ] I have added positive and negative test cases covering my changes.
20
+ - [ ] All checks pass: `uv run ruff format --check . && uv run ruff check . && uv run pyright && uv run pytest`.
21
+ - [ ] Code coverage remains >= 90%.
22
+ - [ ] No emojis have been introduced in outputs or codebase.
23
+ - [ ] My commits adhere to the [Conventional Commits](https://www.conventionalcommits.org/) format.
@@ -0,0 +1,47 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ name: Python ${{ matrix.python-version }} on ${{ matrix.os }}
12
+ runs-on: ${{ matrix.os }}
13
+ strategy:
14
+ fail-fast: false
15
+ matrix:
16
+ os: [ubuntu-latest]
17
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
18
+
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+
22
+ - name: Install uv
23
+ uses: astral-sh/setup-uv@v3
24
+ with:
25
+ version: "latest"
26
+ enable-cache: true
27
+
28
+ - name: Set up Python ${{ matrix.python-version }}
29
+ run: uv python install ${{ matrix.python-version }}
30
+
31
+ - name: Install dependencies
32
+ run: uv sync --all-extras --dev
33
+
34
+ - name: Check code formatting (ruff format)
35
+ run: uv run ruff format --check .
36
+
37
+ - name: Lint codebase (ruff check)
38
+ run: uv run ruff check .
39
+
40
+ - name: Strict type check (pyright)
41
+ run: uv run pyright
42
+
43
+ - name: Run test suite with coverage
44
+ run: uv run pytest --cov=mockrot --cov-branch --cov-report=term-missing --cov-fail-under=90
45
+
46
+ - name: Build distribution package
47
+ run: uv build
@@ -0,0 +1,33 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ pypi-publish:
9
+ name: Build and publish to PyPI
10
+ runs-on: ubuntu-latest
11
+ environment:
12
+ name: pypi
13
+ url: https://pypi.org/p/pytest-mockrot
14
+ permissions:
15
+ id-token: write
16
+
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+
20
+ - name: Install uv
21
+ uses: astral-sh/setup-uv@v3
22
+ with:
23
+ version: "latest"
24
+ enable-cache: true
25
+
26
+ - name: Set up Python
27
+ run: uv python install 3.12
28
+
29
+ - name: Build distributions
30
+ run: uv build
31
+
32
+ - name: Publish package distributions to PyPI
33
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,51 @@
1
+ # Python bytecode and caches
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+
7
+ # Environments
8
+ .venv/
9
+ env/
10
+ venv/
11
+ ENV/
12
+
13
+ # Distribution & build
14
+ build/
15
+ develop-eggs/
16
+ dist/
17
+ downloads/
18
+ eggs/
19
+ .eggs/
20
+ lib/
21
+ lib64/
22
+ parts/
23
+ sdist/
24
+ var/
25
+ wheels/
26
+ *.egg-info/
27
+ .installed.cfg
28
+ *.egg
29
+
30
+ # Testing & coverage
31
+ .pytest_cache/
32
+ .coverage
33
+ .coverage.*
34
+ htmlcov/
35
+ coverage.xml
36
+
37
+ # Linters & type checkers
38
+ .ruff_cache/
39
+ .mypy_cache/
40
+ .pytype/
41
+
42
+ # IDEs & OS
43
+ .vscode/
44
+ .idea/
45
+ *.swp
46
+ *.swo
47
+ *~
48
+ .DS_Store
49
+
50
+ # Mockrot lock files
51
+ .mockrot.lock
@@ -0,0 +1,394 @@
1
+ # AGENTS.md — Mockrot
2
+
3
+ > **Source unique de vérité** pour tout agent IA (Claude Code, Antigravity, Cursor, Codex…) et tout
4
+ > contributeur humain. La spécification produit complète se trouve dans
5
+ > [`cahierdecharge.md`](./cahierdecharge.md). Ce fichier définit **comment** on travaille ; le cahier
6
+ > des charges définit **quoi** construire.
7
+ >
8
+ > **Règle d'or :** en cas d'ambiguïté, de contradiction ou de décision structurante non couverte ici,
9
+ > **s'arrêter et demander** plutôt que supposer.
10
+
11
+ ---
12
+
13
+ ## 1. Contexte du projet
14
+
15
+ **Mockrot** (`pytest-mockrot`) est un plugin `pytest` qui détecte les **mocks obsolètes
16
+ (« Rotting Mocks »)** : des mocks dont les arguments d'appel ou les valeurs de retour ne
17
+ correspondent plus au contrat réel (signature + type de retour) du code de production.
18
+
19
+ - **Le problème :** `unittest.mock` (même avec `autospec=True`) ne valide jamais que `return_value`
20
+ respecte l'annotation `-> ReturnType` de la vraie fonction. Les tests restent verts sur des données
21
+ figées depuis des mois, la prod casse (`AttributeError`, `KeyError`, `ValidationError`).
22
+ - **La solution :** intercepter chaque mock, extraire le contrat de la cible réelle
23
+ (`inspect.signature` + `typing.get_type_hints`), et valider **entrées ET sorties** à chaque appel
24
+ via `pydantic.TypeAdapter`.
25
+ - **Promesse produit :** zéro configuration (`pytest --mockrot`), zéro faux positif bruyant,
26
+ overhead négligeable, ne casse jamais la suite de l'utilisateur.
27
+ - **Cible :** projets Python 3.10+ typés (Pydantic, dataclass, TypedDict), et garde-fou pour le code
28
+ généré/refactoré par des agents IA.
29
+
30
+ ### Périmètre
31
+
32
+ | Version | Exigences | Contenu |
33
+ | :--- | :--- | :--- |
34
+ | **V1 (MVP)** | F1 → F8 | Interception, validation signature, validation retour, async, `side_effect`, champs zombies, modes `warn`/`strict`, exemptions |
35
+ | **V1.1** | F9 → F12 | Mode record (`.mockrot.lock`), mocks HTTP/OpenAPI, auto-fixer LibCST, export GitHub/JSON |
36
+
37
+ **Ne jamais implémenter une exigence V1.1 tant que la V1 n'est pas livrée et validée**, sauf demande
38
+ explicite.
39
+
40
+ ### Plan de réalisation
41
+
42
+ 1. **Semaine 1 :** bootstrap du dépôt, `interceptor`, `extractor`, `validator`, support async.
43
+ 2. **Semaine 2 :** champs zombies, sous-mocks `MagicMock`, `reporter` Rich.
44
+ 3. **Semaine 3 :** `recorder`, export GitHub, démo, publication PyPI v0.1.0.
45
+
46
+ ---
47
+
48
+ ## 2. Stack technique
49
+
50
+ | Rôle | Outil | Contrainte |
51
+ | :--- | :--- | :--- |
52
+ | Langage | Python | `>= 3.10` (CI : 3.10 → 3.13) |
53
+ | Projet & build | `uv` + `hatchling` | `pyproject.toml` PEP 621, `src-layout`, `uv.lock` versionné |
54
+ | Plugin | `pytest` | `>= 8.0`, entry-point `pytest11` |
55
+ | Validation | `pydantic` v2 | `TypeAdapter` uniquement, jamais d'API v1 |
56
+ | Rendu terminal | `rich` | Uniquement dans `reporter.py` |
57
+ | Auto-fixer | `libcst` | Extra optionnel `pytest-mockrot[fix]` (dépendance lourde) |
58
+ | Lint & format | `ruff` | Zéro warning |
59
+ | Typage | `pyright` mode **strict** | Zéro erreur |
60
+ | Tests | `pytest`, `pytester`, `pytest-cov`, `pytest-mock` | Couverture ≥ 90 % (lignes + branches) |
61
+
62
+ - Nom de distribution : `pytest-mockrot` — package importable : `mockrot`.
63
+ - **Aucune nouvelle dépendance runtime** sans justification écrite et validation explicite. Chaque
64
+ dépendance runtime est un coût imposé à tous les utilisateurs.
65
+
66
+ ---
67
+
68
+ ## 3. Architecture
69
+
70
+ ### Pipeline
71
+
72
+ ```text
73
+ pytest ──▶ plugin.py ──▶ interceptor.py ──▶ extractor.py ──▶ validator.py ──▶ reporter.py
74
+ (hooks) (capture cible) (contrat) (violations) (affichage)
75
+ ```
76
+
77
+ ### Responsabilités et règles de dépendance
78
+
79
+ | Module | Responsabilité | Peut importer |
80
+ | :--- | :--- | :--- |
81
+ | `plugin.py` | Hooks pytest, options CLI, config `[tool.mockrot]`, **seule frontière fail-safe** | tout |
82
+ | `interceptor.py` | Wrap de `unittest.mock._patch` et `MonkeyPatch.setattr`, proxy d'appel | `extractor`, `validator`, `models` |
83
+ | `extractor.py` | Signature, type hints, `ForwardRef`/`TYPE_CHECKING`, async, cache | `models` |
84
+ | `validator.py` | Validation des arguments et du retour, champs zombies | `extractor`, `models` |
85
+ | `reporter.py` | Rendu Rich, export JSON / SARIF / GitHub | `models` |
86
+ | `recorder.py` | Enregistrement d'empreintes `.mockrot.lock` (V1.1) | `models` |
87
+ | `fixer.py` | Réécriture LibCST (V1.1) | `models` |
88
+ | `models.py` | Types de domaine (`Contract`, `Violation`, `MockSite`…) en `dataclass(frozen=True, slots=True)` | stdlib uniquement |
89
+
90
+ Règles :
91
+
92
+ - **Seul `plugin.py` importe `pytest`.** Le cœur (`extractor`, `validator`) est testable sans pytest.
93
+ - **Seul `reporter.py` importe `rich`.** Seul `fixer.py` importe `libcst`.
94
+ - **Seul `interceptor.py` touche aux API privées** (`unittest.mock._patch`). Toute dépendance à une
95
+ API privée est isolée, commentée, et testée sur toute la matrice Python.
96
+ - Les modules communiquent via les types de `models.py`, jamais via des `str` ou des `dict` ad hoc.
97
+ - Pas de dépendance circulaire. Pas d'état global mutable, sauf le registre de session explicitement
98
+ créé et détruit par `plugin.py`.
99
+
100
+ ### Structure cible du dépôt
101
+
102
+ ```text
103
+ mockrot/
104
+ ├── pyproject.toml
105
+ ├── uv.lock
106
+ ├── AGENTS.md / CLAUDE.md / README.md / CHANGELOG.md / LICENSE
107
+ ├── .github/workflows/ci.yml
108
+ ├── src/mockrot/
109
+ │ ├── __init__.py plugin.py interceptor.py extractor.py
110
+ │ ├── validator.py reporter.py models.py errors.py
111
+ │ ├── recorder.py fixer.py # V1.1
112
+ │ └── py.typed
113
+ └── tests/
114
+ ├── unit/ # cœur pur, sans plugin pytest
115
+ ├── integration/ # plugin de bout en bout via la fixture `pytester`
116
+ └── fixtures/ # faux modules « production » (sync, async, pydantic, dataclass, TypedDict…)
117
+ ```
118
+
119
+ Toute déviation par rapport à cette structure doit être justifiée et validée.
120
+
121
+ ---
122
+
123
+ ## 4. Invariants produit (non négociables)
124
+
125
+ 1. **Ne jamais faire planter la suite de tests de l'utilisateur.** Toute erreur interne est capturée
126
+ à une **unique frontière** dans `plugin.py`, convertie en avertissement « mode dégradé », et le test
127
+ continue normalement.
128
+ 2. **Transparence totale.** Le proxy ne modifie jamais la valeur retournée, l'exception levée, ni le
129
+ comportement du mock. En mode `warn`, un test doit produire exactement le même résultat avec ou
130
+ sans Mockrot.
131
+ 3. **Zéro effet de bord quand désactivé.** Sans `--mockrot`, aucun patch n'est installé, aucun import
132
+ coûteux n'est effectué. Tous les patchs sont restaurés en fin de session, de manière idempotente.
133
+ 4. **Faux négatif > faux positif.** En cas de doute (type non résolu, signature indisponible, cible
134
+ dynamique), on passe en mode dégradé silencieux. La confiance est le produit.
135
+ 5. **Zéro configuration.** Tout fonctionne sans `[tool.mockrot]`. La config ne fait qu'affiner.
136
+ 6. **Budget de performance.** Contrats et `TypeAdapter` compilés **une seule fois par cible**.
137
+ Objectif : overhead < 15 ms sur 500 tests (seuil d'alerte : 50 ms). Toute régression est mesurée.
138
+ 7. **Chaque violation est actionnable.** Fichier + ligne du mock, cible, contrat réel, et diff précis.
139
+
140
+ ---
141
+
142
+ ## 5. Pièges techniques connus (à lire avant de coder)
143
+
144
+ Le code de la §6 du cahier des charges est **indicatif**, pas à copier tel quel. Points identifiés :
145
+
146
+ - **`_patch.start()` ne suffit pas.** `@patch` en décorateur et `with patch(...)` passent par
147
+ `_patch.__enter__` (que `start()` appelle). Il faut intercepter `__enter__` pour couvrir les trois
148
+ usages et `pytest-mock`.
149
+ - **Mode lax de Pydantic = faux négatifs.** `validate_python("5000", strict=False)` accepte `"5000"`
150
+ pour un `int`. Le rapport attendu signale ce cas comme « Type mismatch » : la validation des valeurs
151
+ mockées doit être **stricte** (`strict=True`), avec les adaptations nécessaires (ex. `dict` → modèle).
152
+ - **Champs zombies.** `TypeAdapter(BaseModel)` ignore les clés en trop par défaut. Ne **jamais**
153
+ muter la config du modèle utilisateur : calculer les clés obsolètes séparément (`model_fields`,
154
+ alias, `TypedDict.__annotations__` / `__required_keys__`).
155
+ - **Cache.** `lru_cache` sur `real_fn` échoue sur les callables non hashables, et un cache par
156
+ `id()` est faux (réutilisation des `id` après GC). Utiliser un cache par identité robuste
157
+ (`WeakKeyDictionary` avec repli) et garder `lru_cache` pour les cas hashables.
158
+ - **`self` / `cls`.** Utiliser `inspect.getattr_static` pour distinguer fonction, `staticmethod`,
159
+ `classmethod`, méthode liée ; ajuster la signature avant `bind()`.
160
+ - **`side_effect` itérable.** Ne jamais consommer l'itérateur par anticipation : valider chaque
161
+ valeur au moment où elle est produite. Exceptions (classes **et** instances) = toujours légitimes.
162
+ - **`AsyncMock`.** Valider la valeur **résolue après `await`**, jamais la coroutine.
163
+ - **Mocks déjà spécifiés.** `autospec=True` / `spec=` : ne pas dupliquer les erreurs de signature
164
+ déjà levées par `unittest.mock`.
165
+ - **Cibles non fonctionnelles.** Patch d'une classe, d'une constante, d'une propriété, d'un module :
166
+ définir le contrat explicitement (ex. classe → `__init__` + instance) ou ignorer proprement.
167
+ - **`monkeypatch.setattr` avec un vrai callable** (lambda, fonction factice) : même validation que
168
+ pour un `Mock`, via un wrapper transparent.
169
+ - **`ForwardRef` / `TYPE_CHECKING`.** Résolution via le `globalns` du module, puis repli AST isolé
170
+ et mis en cache. Échec → mode dégradé, jamais d'exception.
171
+ - **`pytest-xdist`.** Le rapport doit à terme agréger les résultats des workers : ne pas coder un
172
+ registre qui suppose un processus unique sans l'isoler derrière une interface.
173
+
174
+ - **`inspect.stack()` vs `sys._getframe`.** `inspect.stack()` effectue des appels `os.stat`
175
+ sur chaque fichier de la pile pour lire le code source, ce qui brise les suites asynchrones
176
+ vérifiant les I/O bloquantes (ex. `blockbuster.BlockingError` dans Starlette) et introduit un
177
+ surcoût majeur. Utiliser `sys._getframe()` pour une inspection purement mémoire sans I/O.
178
+ - **Plugins pytest internes utilisant `MonkeyPatch.setattr`.** `pytest-asyncio` monkeypatche
179
+ `item.obj` pour synchroniser l'exécution des tests coroutines. Le hook `_patched_monkeypatch_setattr`
180
+ doit ignorer les appels issus des plugins internes de test runner (`pytest_asyncio`, `_pytest`,
181
+ `pluggy`) pour ne pas valider à tort les fonctions de test elles-mêmes comme des mocks.
182
+
183
+ Tout nouveau piège découvert est **ajouté à cette section** dans le même commit que son correctif.
184
+
185
+ ---
186
+
187
+ ## 6. Méthode de travail (niveau senior)
188
+
189
+ ### Cycle pour chaque tâche
190
+
191
+ 1. **Comprendre** — relire l'exigence (F-ID) dans le cahier des charges, lire le code existant
192
+ concerné, identifier les cas limites. Reformuler l'objectif en une phrase.
193
+ 2. **Planifier** — pour toute tâche touchant plus d'un module ou l'API publique : présenter un plan
194
+ court (fichiers impactés, approche, tests prévus, risques) et **attendre validation**.
195
+ 3. **Test rouge** — écrire d'abord le test qui échoue (détection attendue **et** absence de faux
196
+ positif). Pour un bug : un test qui reproduit le bug avant tout correctif.
197
+ 4. **Implémenter** — le minimum pour passer au vert. Pas de code spéculatif (YAGNI).
198
+ 5. **Refactorer** — nommage, duplication, lisibilité, sans changer le comportement.
199
+ 6. **Vérifier** — lancer la vérification complète (§10). Rien n'est « fini » sans preuve.
200
+ 7. **Commiter** — commits atomiques et propres (§8).
201
+
202
+ ### Principes
203
+
204
+ - **Petits incréments.** Une branche = un objectif. Une PR relisible en moins de 15 minutes
205
+ (idéalement < 400 lignes de diff hors tests et fixtures).
206
+ - **Pas de changement hors périmètre.** Ne pas reformater, renommer ou « améliorer » du code non lié
207
+ à la tâche. Les améliorations repérées sont notées et proposées séparément.
208
+ - **Lire avant d'écrire.** Réutiliser les abstractions existantes avant d'en créer de nouvelles.
209
+ - **Décisions tracées.** Toute décision d'architecture non triviale est consignée dans
210
+ `docs/adr/NNNN-titre.md` (contexte, décision, alternatives, conséquences).
211
+ - **Honnêteté.** Ne jamais affirmer qu'un test passe sans l'avoir exécuté. Signaler explicitement
212
+ ce qui n'a pas été vérifié, les compromis faits, et les limites connues.
213
+ - **Dogfooding.** Dès que F1–F3 sont livrés, la suite de tests de Mockrot tourne elle-même avec
214
+ `--mockrot=strict` en CI.
215
+
216
+ ---
217
+
218
+ ## 7. Standards de code
219
+
220
+ ### Langue
221
+
222
+ - **Anglais** pour tout ce qui est versionné et public : code, identifiants, docstrings,
223
+ commentaires, messages d'erreur, commits, branches, PR, README, CHANGELOG.
224
+ - **Français** pour les échanges avec l'utilisateur et la documentation interne (`AGENTS.md`,
225
+ `CLAUDE.md`, cahier des charges).
226
+
227
+ ### Python
228
+
229
+ - `from __future__ import annotations` en tête de chaque module.
230
+ - **Typage complet**, `pyright --strict` à zéro erreur. `Any` uniquement aux frontières (valeurs
231
+ mockées arbitraires) et jamais propagé. `# type: ignore[code]` / `# pyright: ignore[rule]`
232
+ interdit sans commentaire justificatif sur la même ligne.
233
+ - Types de domaine : `@dataclass(frozen=True, slots=True)`. Énumérations : `enum.Enum`/`StrEnum`
234
+ (avec repli 3.10).
235
+ - Fonctions courtes, pures dès que possible, une seule responsabilité. Pas d'effets de bord à
236
+ l'import.
237
+ - **Exceptions :** hiérarchie dédiée dans `errors.py` (`MockrotError` → …). Jamais de `except:` nu
238
+ ni de `except Exception` hors de la frontière fail-safe de `plugin.py` (et du repli documenté de
239
+ `extractor.py`). Toujours chaîner (`raise ... from exc`).
240
+ - Pas de `print()`. Sortie utilisateur via `reporter.py` ; diagnostic interne via `logging`
241
+ (logger `mockrot`) ou `warnings`.
242
+ - Docstrings style Google sur toute l'API publique et les fonctions non triviales. Les commentaires
243
+ expliquent le **pourquoi**, pas le quoi.
244
+ - Pas de code mort, pas de code commenté, pas de `TODO` sans référence (`TODO(F9): ...`).
245
+ - Ruff (base) : `E, W, F, I, B, UP, SIM, RUF, PT, N, C4, TC, ARG, BLE, ERA, PL`. Format : `ruff format`.
246
+
247
+ ---
248
+
249
+ ## 8. Workflow Git
250
+
251
+ ### Branches
252
+
253
+ - **`main`** : protégée, toujours verte, toujours publiable. **Aucun commit direct.** Historique
254
+ **linéaire** (pas de merge commits).
255
+ - Branches courtes (durée de vie ≤ 2–3 jours), créées depuis `main` à jour :
256
+
257
+ ```text
258
+ <type>/<scope-ou-fid>-<description-kebab-case>
259
+
260
+ feat/f2-signature-validation
261
+ feat/f4-async-mock-support
262
+ fix/extractor-forward-ref-name-error
263
+ refactor/validator-split-zombie-detection
264
+ test/integration-side-effect-iterables
265
+ ci/python-matrix
266
+ docs/readme-quickstart
267
+ chore/bootstrap-uv-project
268
+ ```
269
+
270
+ - Une branche = un sujet. Rebaser régulièrement : `git fetch origin && git rebase origin/main`.
271
+ - Supprimer la branche après merge.
272
+
273
+ ### Commits — Conventional Commits 1.0
274
+
275
+ ```text
276
+ <type>(<scope>): <résumé impératif, minuscules, sans point, ≤ 72 caractères>
277
+
278
+ <corps optionnel : POURQUOI ce changement, contexte, alternatives écartées.
279
+ Lignes ≤ 72 caractères.>
280
+
281
+ <footer optionnel : Refs: F2 | Closes #12 | BREAKING CHANGE: ...>
282
+ ```
283
+
284
+ - **Types :** `feat`, `fix`, `refactor`, `perf`, `test`, `docs`, `build`, `ci`, `chore`, `style`,
285
+ `revert`.
286
+ - **Scopes :** `plugin`, `interceptor`, `extractor`, `validator`, `reporter`, `recorder`, `fixer`,
287
+ `models`, `config`, `deps`, `ci`, `release`.
288
+
289
+ Exemples :
290
+
291
+ ```text
292
+ feat(interceptor): wrap _patch.__enter__ to capture original target
293
+ fix(validator): validate scalar return values in strict mode
294
+ test(validator): cover zombie keys on nested TypedDict
295
+ refactor(extractor): extract self/cls stripping into dedicated helper
296
+ perf(extractor): cache TypeAdapter per target with weak references
297
+ build(deps): add pydantic>=2.7 as runtime dependency
298
+ ```
299
+
300
+ Règles :
301
+
302
+ - **Atomique :** un commit = un changement logique cohérent. Chaque commit passe la vérification
303
+ complète (historique *bisectable*).
304
+ - Ne jamais mélanger refactor, formatage et changement de comportement dans le même commit.
305
+ - Le code et ses tests vont **dans le même commit**.
306
+ - Interdits : `wip`, `fix`, `update`, `misc`, `changes`, messages vagues ou en français.
307
+ - Avant d'ouvrir une PR : nettoyer l'historique de **sa propre branche** (`git rebase -i`,
308
+ `git commit --fixup` + `--autosquash`).
309
+
310
+ ### Pull Requests
311
+
312
+ - Titre = message Conventional Commit (il devient le commit sur `main`).
313
+ - Description : **Quoi / Pourquoi / Comment / Comment tester / Risques**, F-ID référencée.
314
+ - Merge uniquement si CI verte (lint, types, tests matrice, couverture). Stratégie : **squash merge**.
315
+
316
+ ### Releases
317
+
318
+ - **SemVer** (`0.y.z` tant que l'API n'est pas stable). Tags annotés `vX.Y.Z` sur `main`.
319
+ - `CHANGELOG.md` au format *Keep a Changelog*, alimenté depuis les commits.
320
+
321
+ ### Interdits absolus
322
+
323
+ - `git push` sur `main`, `git push --force` (seul `--force-with-lease` sur sa propre branche, après
324
+ accord), `--no-verify`, réécriture d'un historique partagé.
325
+ - `git add -A` / `git add .` à l'aveugle : ajouter des chemins explicites et relire
326
+ `git diff --staged` avant chaque commit.
327
+ - Commiter des secrets, `.env`, caches (`.venv`, `__pycache__`, `.pytest_cache`, `.ruff_cache`),
328
+ ou des fichiers sans rapport avec la tâche.
329
+ - Toute commande destructive (`reset --hard`, `clean -fd`, `branch -D`) sans accord explicite.
330
+
331
+ ---
332
+
333
+ ## 9. Stratégie de tests
334
+
335
+ - **Unitaires (`tests/unit/`)** : `extractor`, `validator`, `models` testés en isolation, sans
336
+ charger le plugin.
337
+ - **Intégration (`tests/integration/`)** : plugin de bout en bout via la fixture officielle
338
+ **`pytester`** (fichiers de test générés, assertions sur la sortie et le code de retour).
339
+ - **Chaque détection a son test négatif** : un test prouvant l'absence de faux positif sur un mock
340
+ correct équivalent.
341
+ - Couvrir systématiquement : fonction, méthode d'instance, `classmethod`, `staticmethod`, `async def`,
342
+ `BaseModel`, `dataclass`, `TypedDict`, `Union`/`Optional`, génériques, `side_effect` (itérable,
343
+ callable, exception), `ForwardRef`, `TYPE_CHECKING`, builtins C sans signature.
344
+ - Noms explicites : `test_<comportement>_when_<condition>` (ex.
345
+ `test_reports_zombie_key_when_dict_has_removed_field`).
346
+ - Tests déterministes, sans réseau, sans dépendance à l'ordre. Paramétrer (`pytest.mark.parametrize`)
347
+ plutôt que dupliquer.
348
+ - Couverture ≥ 90 % lignes + branches sur `src/mockrot`. Pas de test écrit uniquement pour la
349
+ couverture.
350
+ - Benchmark de l'overhead sur une suite synthétique de 500 tests avant chaque release.
351
+
352
+ ---
353
+
354
+ ## 10. Commandes
355
+
356
+ ```bash
357
+ uv sync # installer l'environnement
358
+ uv run ruff format . # formater
359
+ uv run ruff check . # linter (ajouter --fix si pertinent)
360
+ uv run pyright # typage strict
361
+ uv run pytest # tests
362
+ uv run pytest --cov=mockrot --cov-branch # tests + couverture
363
+ ```
364
+
365
+ **Vérification complète (obligatoire avant chaque commit) :**
366
+
367
+ ```bash
368
+ uv run ruff format --check . && uv run ruff check . && uv run pyright && uv run pytest
369
+ ```
370
+
371
+ ---
372
+
373
+ ## 11. Definition of Done
374
+
375
+ Une tâche est terminée **uniquement si** :
376
+
377
+ - [ ] L'exigence (F-ID) est satisfaite, cas limites inclus.
378
+ - [ ] Tests ajoutés : cas positif **et** test négatif (pas de faux positif).
379
+ - [ ] `ruff format`, `ruff check`, `pyright --strict`, `pytest` : tous au vert.
380
+ - [ ] Couverture ≥ 90 % maintenue.
381
+ - [ ] Les invariants de la §4 sont respectés (fail-safe, transparence, perf).
382
+ - [ ] Docstrings / README / CHANGELOG mis à jour si le comportement visible change.
383
+ - [ ] Nouveau piège technique documenté en §5 le cas échéant.
384
+ - [ ] Commits atomiques, Conventional Commits, branche propre et rebasée sur `main`.
385
+
386
+ ---
387
+
388
+ ## 12. Décisions tranchées et actées
389
+
390
+ - **CLI autonome** : module léger `cli.py` (point d'entrée console `mockrot = "mockrot.cli:main"` dans `pyproject.toml`) qui délègue à `pytest` avec `--mockrot` activé, garantissant le fonctionnement sans friction de `uvx mockrot`.
391
+ - **Budget de performance** : overhead du moteur de validation pur ciblé à **< 15 ms** sur 500 tests ; seuil d'alerte global de la suite de tests (incluant le rapport terminal Rich) fixé à **< 50 ms**.
392
+ - **Mode `accessed-only`** : planifié pour la V1.1 (`--mockrot-mode=accessed-only`) pour éliminer le bruit sur les vieilles bases de code sans compliquer le MVP V1.
393
+ - **LibCST** : confirmé en extra optionnel `pytest-mockrot[fix]` pour garder le package principal ultraléger.
394
+ - **Licence** : licence **MIT**.
@@ -0,0 +1,21 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2026-10-16
9
+
10
+ ### Added
11
+ - **Core Interception (F1):** Transparent proxy for `unittest.mock._patch` (`@patch`, `with patch(...)`, `.start()`) and `pytest.MonkeyPatch.setattr` with fast, non-blocking stack inspection via `sys._getframe`.
12
+ - **Signature Validation (F2):** Runtime signature contract extraction via `inspect.signature` with `self`/`cls` parameter binding and keyword argument validation.
13
+ - **Return Type Validation (F3):** Strict runtime type validation of scalar types, Pydantic v2 models, dataclasses, and TypedDicts powered by `pydantic.TypeAdapter`.
14
+ - **Async & Coroutine Support (F4):** Interception and validation of `async def` targets, coroutines, and `AsyncMock` post-await values.
15
+ - **Side Effect Handling (F5):** Dynamic per-item validation for `side_effect` generators, iterables, and callables, with built-in immunity for intentional exception simulation.
16
+ - **Zombie Field Detection (F6):** Automatic detection of deleted or renamed keys in mocked dictionaries for `BaseModel`, `dataclass`, and `TypedDict`.
17
+ - **Execution Modes (F7):** Non-blocking audit mode (`--mockrot=warn`) and strict CI gatekeeper mode (`--mockrot=strict`).
18
+ - **Exemptions & Filtering (F8):** Line-level `# mockrot: ignore` comments and `@pytest.mark.mockrot_ignore` test marks.
19
+ - **Rich Terminal Reporting:** Clean, emoji-free terminal output with actionable diff tables and line-number diagnostics.
20
+ - **CLI Entry Point:** Standalone `mockrot` command allowing instant execution via `uvx mockrot`.
21
+ - **CI & Quality Assurance:** Complete GitHub Actions test matrix across Python 3.10, 3.11, 3.12, and 3.13 with 92% code coverage and strict Pyright verification.