pytest-uuid 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.
- pytest_uuid-0.1.0/PKG-INFO +692 -0
- pytest_uuid-0.1.0/README.md +659 -0
- pytest_uuid-0.1.0/pyproject.toml +151 -0
- pytest_uuid-0.1.0/src/pytest_uuid/__init__.py +56 -0
- pytest_uuid-0.1.0/src/pytest_uuid/_tracking.py +197 -0
- pytest_uuid-0.1.0/src/pytest_uuid/api.py +384 -0
- pytest_uuid-0.1.0/src/pytest_uuid/config.py +174 -0
- pytest_uuid-0.1.0/src/pytest_uuid/generators.py +183 -0
- pytest_uuid-0.1.0/src/pytest_uuid/plugin.py +407 -0
- pytest_uuid-0.1.0/src/pytest_uuid/py.typed +0 -0
- pytest_uuid-0.1.0/src/pytest_uuid/types.py +227 -0
|
@@ -0,0 +1,692 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pytest-uuid
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A pytest plugin for mocking uuid.uuid4() calls
|
|
5
|
+
Keywords: pytest,plugin,uuid,mock,testing
|
|
6
|
+
Author: CaptainDriftwood
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Framework :: Pytest
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Classifier: Topic :: Software Development :: Testing
|
|
23
|
+
Classifier: Topic :: Software Development :: Testing :: Mocking
|
|
24
|
+
Classifier: Topic :: Software Development :: Testing :: Unit
|
|
25
|
+
Requires-Dist: pytest>=7.0.0
|
|
26
|
+
Requires-Dist: tomli>=2.0.0 ; python_full_version < '3.11'
|
|
27
|
+
Requires-Python: >=3.9
|
|
28
|
+
Project-URL: Documentation, https://github.com/CaptainDriftwood/pytest-uuid#readme
|
|
29
|
+
Project-URL: Homepage, https://github.com/CaptainDriftwood/pytest-uuid
|
|
30
|
+
Project-URL: Issues, https://github.com/CaptainDriftwood/pytest-uuid/issues
|
|
31
|
+
Project-URL: Repository, https://github.com/CaptainDriftwood/pytest-uuid
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
<p align="center">
|
|
35
|
+
<img src="docs/images/logo.svg" alt="pytest-uuid logo" width="300">
|
|
36
|
+
</p>
|
|
37
|
+
|
|
38
|
+
<h1 align="center">pytest-uuid</h1>
|
|
39
|
+
|
|
40
|
+
A pytest plugin for mocking `uuid.uuid4()` calls in your tests.
|
|
41
|
+
|
|
42
|
+
[](https://pypi.org/project/pytest-uuid/)
|
|
43
|
+
[](https://opensource.org/licenses/MIT)
|
|
44
|
+
[](https://github.com/CaptainDriftwood/pytest-uuid/actions/workflows/test.yml)
|
|
45
|
+
[](https://codecov.io/gh/CaptainDriftwood/pytest-uuid)
|
|
46
|
+
[](https://github.com/astral-sh/ruff)
|
|
47
|
+
[](https://github.com/astral-sh/uv)
|
|
48
|
+
[](https://github.com/astral-sh/ty)
|
|
49
|
+
[](https://docs.pytest.org/)
|
|
50
|
+
|
|
51
|
+

|
|
52
|
+
|
|
53
|
+
- [Features](#features)
|
|
54
|
+
- [Installation](#installation)
|
|
55
|
+
- [Quick Start](#quick-start)
|
|
56
|
+
- [Usage](#usage)
|
|
57
|
+
- [API Reference](#api-reference)
|
|
58
|
+
- [Development](#development)
|
|
59
|
+
- [License](#license)
|
|
60
|
+
|
|
61
|
+
## Features
|
|
62
|
+
|
|
63
|
+
- Mock `uuid.uuid4()` with deterministic values in your tests
|
|
64
|
+
- Works with both `import uuid` and `from uuid import uuid4` patterns
|
|
65
|
+
- Multiple ways to mock: static, sequence, seeded, or node-seeded
|
|
66
|
+
- Decorator, marker, and fixture APIs (inspired by freezegun)
|
|
67
|
+
- Configurable exhaustion behavior for sequences
|
|
68
|
+
- Ignore list for packages that should use real UUIDs
|
|
69
|
+
- Spy mode to track calls without mocking
|
|
70
|
+
- Detailed call tracking with caller module/file info
|
|
71
|
+
- Automatic cleanup after each test
|
|
72
|
+
- Zero configuration required - just use the fixture
|
|
73
|
+
|
|
74
|
+
## Installation
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pip install pytest-uuid
|
|
78
|
+
|
|
79
|
+
# or with uv
|
|
80
|
+
uv add pytest-uuid
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Quick Start
|
|
84
|
+
|
|
85
|
+
### Fixture API
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
import uuid
|
|
89
|
+
|
|
90
|
+
def test_single_uuid(mock_uuid):
|
|
91
|
+
mock_uuid.set("12345678-1234-5678-1234-567812345678")
|
|
92
|
+
assert str(uuid.uuid4()) == "12345678-1234-5678-1234-567812345678"
|
|
93
|
+
|
|
94
|
+
def test_multiple_uuids(mock_uuid):
|
|
95
|
+
mock_uuid.set(
|
|
96
|
+
"11111111-1111-1111-1111-111111111111",
|
|
97
|
+
"22222222-2222-2222-2222-222222222222",
|
|
98
|
+
)
|
|
99
|
+
assert str(uuid.uuid4()) == "11111111-1111-1111-1111-111111111111"
|
|
100
|
+
assert str(uuid.uuid4()) == "22222222-2222-2222-2222-222222222222"
|
|
101
|
+
# Cycles back to the first UUID
|
|
102
|
+
assert str(uuid.uuid4()) == "11111111-1111-1111-1111-111111111111"
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Decorator API
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
import uuid
|
|
109
|
+
from pytest_uuid import freeze_uuid
|
|
110
|
+
|
|
111
|
+
@freeze_uuid("12345678-1234-5678-1234-567812345678")
|
|
112
|
+
def test_with_decorator():
|
|
113
|
+
assert str(uuid.uuid4()) == "12345678-1234-5678-1234-567812345678"
|
|
114
|
+
|
|
115
|
+
@freeze_uuid(seed=42)
|
|
116
|
+
def test_seeded():
|
|
117
|
+
# Reproducible UUIDs from seed
|
|
118
|
+
result = uuid.uuid4()
|
|
119
|
+
assert result.version == 4
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Marker API
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
import uuid
|
|
126
|
+
import pytest
|
|
127
|
+
|
|
128
|
+
@pytest.mark.freeze_uuid("12345678-1234-5678-1234-567812345678")
|
|
129
|
+
def test_with_marker():
|
|
130
|
+
assert str(uuid.uuid4()) == "12345678-1234-5678-1234-567812345678"
|
|
131
|
+
|
|
132
|
+
@pytest.mark.freeze_uuid(seed="node")
|
|
133
|
+
def test_node_seeded():
|
|
134
|
+
# Same test always gets the same UUIDs
|
|
135
|
+
result = uuid.uuid4()
|
|
136
|
+
assert result.version == 4
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Usage
|
|
140
|
+
|
|
141
|
+
### Static UUIDs
|
|
142
|
+
|
|
143
|
+
Return the same UUID every time:
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
def test_static(mock_uuid):
|
|
147
|
+
mock_uuid.set("12345678-1234-5678-1234-567812345678")
|
|
148
|
+
assert uuid.uuid4() == uuid.uuid4() # Same UUID
|
|
149
|
+
|
|
150
|
+
# Or with decorator
|
|
151
|
+
@freeze_uuid("12345678-1234-5678-1234-567812345678")
|
|
152
|
+
def test_static_decorator():
|
|
153
|
+
assert uuid.uuid4() == uuid.uuid4() # Same UUID
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### UUID Sequences
|
|
157
|
+
|
|
158
|
+
Return UUIDs from a list:
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
def test_sequence(mock_uuid):
|
|
162
|
+
mock_uuid.set(
|
|
163
|
+
"11111111-1111-1111-1111-111111111111",
|
|
164
|
+
"22222222-2222-2222-2222-222222222222",
|
|
165
|
+
)
|
|
166
|
+
assert str(uuid.uuid4()) == "11111111-1111-1111-1111-111111111111"
|
|
167
|
+
assert str(uuid.uuid4()) == "22222222-2222-2222-2222-222222222222"
|
|
168
|
+
# Cycles back by default
|
|
169
|
+
assert str(uuid.uuid4()) == "11111111-1111-1111-1111-111111111111"
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Seeded UUIDs
|
|
173
|
+
|
|
174
|
+
Generate reproducible UUIDs from a seed:
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
def test_seeded(mock_uuid):
|
|
178
|
+
mock_uuid.set_seed(42)
|
|
179
|
+
first = uuid.uuid4()
|
|
180
|
+
|
|
181
|
+
mock_uuid.set_seed(42) # Reset to same seed
|
|
182
|
+
assert uuid.uuid4() == first # Same UUID
|
|
183
|
+
|
|
184
|
+
# With decorator
|
|
185
|
+
@freeze_uuid(seed=42)
|
|
186
|
+
def test_seeded_decorator():
|
|
187
|
+
result = uuid.uuid4()
|
|
188
|
+
assert result.version == 4 # Valid UUID v4
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Node-Seeded UUIDs (Recommended)
|
|
192
|
+
|
|
193
|
+
Derive the seed from the test's node ID for automatic reproducibility:
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
def test_node_seeded(mock_uuid):
|
|
197
|
+
mock_uuid.set_seed_from_node()
|
|
198
|
+
# Same test always produces the same sequence
|
|
199
|
+
|
|
200
|
+
# With marker
|
|
201
|
+
@pytest.mark.freeze_uuid(seed="node")
|
|
202
|
+
def test_node_seeded_marker():
|
|
203
|
+
# Same test always produces the same sequence
|
|
204
|
+
pass
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
> **Why node seeding is recommended:** Node-seeded UUIDs give you deterministic, reproducible tests without the maintenance burden of hardcoded UUIDs. Each test gets its own unique seed derived from its fully-qualified name (e.g., `test_module.py::TestClass::test_method`), so tests are isolated and don't affect each other. When a test fails, you get the same UUIDs on every run, making debugging easier. Unlike static UUIDs, you never have to update test files when adding new UUID calls.
|
|
208
|
+
|
|
209
|
+
#### Class-Level Node Seeding
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
import uuid
|
|
213
|
+
import pytest
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@pytest.mark.freeze_uuid(seed="node")
|
|
217
|
+
class TestUserService:
|
|
218
|
+
def test_create(self):
|
|
219
|
+
# Seed derived from "test_module.py::TestUserService::test_create"
|
|
220
|
+
result = uuid.uuid4()
|
|
221
|
+
assert result.version == 4
|
|
222
|
+
|
|
223
|
+
def test_update(self):
|
|
224
|
+
# Seed derived from "test_module.py::TestUserService::test_update"
|
|
225
|
+
result = uuid.uuid4()
|
|
226
|
+
assert result.version == 4
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
#### Module-Level Node Seeding
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
# tests/test_user_creation.py
|
|
233
|
+
import uuid
|
|
234
|
+
import pytest
|
|
235
|
+
|
|
236
|
+
pytestmark = pytest.mark.freeze_uuid(seed="node")
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def test_create_user():
|
|
240
|
+
# Seed derived from "test_user_creation.py::test_create_user"
|
|
241
|
+
result = uuid.uuid4()
|
|
242
|
+
assert result.version == 4
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def test_create_admin():
|
|
246
|
+
# Seed derived from "test_user_creation.py::test_create_admin"
|
|
247
|
+
result = uuid.uuid4()
|
|
248
|
+
assert result.version == 4
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
#### Session-Level Node Seeding
|
|
252
|
+
|
|
253
|
+
```python
|
|
254
|
+
# conftest.py
|
|
255
|
+
import pytest
|
|
256
|
+
from pytest_uuid import freeze_uuid
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
@pytest.fixture(scope="session", autouse=True)
|
|
260
|
+
def freeze_uuids_globally(request):
|
|
261
|
+
# Use session node ID as seed for all tests
|
|
262
|
+
seed = hash(request.node.nodeid)
|
|
263
|
+
with freeze_uuid(seed=seed):
|
|
264
|
+
yield
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
> **Note:** For session-level fixtures, use `request.node.nodeid` directly since `seed="node"` in the marker requires per-test context. Alternatively, use a fixed seed for true global determinism.
|
|
268
|
+
|
|
269
|
+
### Exhaustion Behavior
|
|
270
|
+
|
|
271
|
+
Control what happens when a UUID sequence is exhausted:
|
|
272
|
+
|
|
273
|
+
```python
|
|
274
|
+
from pytest_uuid import ExhaustionBehavior, UUIDsExhaustedError
|
|
275
|
+
|
|
276
|
+
def test_exhaustion_raise(mock_uuid):
|
|
277
|
+
mock_uuid.set_exhaustion_behavior("raise")
|
|
278
|
+
mock_uuid.set("11111111-1111-1111-1111-111111111111")
|
|
279
|
+
|
|
280
|
+
uuid.uuid4() # Returns the UUID
|
|
281
|
+
|
|
282
|
+
with pytest.raises(UUIDsExhaustedError):
|
|
283
|
+
uuid.uuid4() # Raises - sequence exhausted
|
|
284
|
+
|
|
285
|
+
# With decorator
|
|
286
|
+
@freeze_uuid(
|
|
287
|
+
["11111111-1111-1111-1111-111111111111"],
|
|
288
|
+
on_exhausted="raise", # or "cycle" or "random"
|
|
289
|
+
)
|
|
290
|
+
def test_exhaustion_decorator():
|
|
291
|
+
uuid.uuid4()
|
|
292
|
+
with pytest.raises(UUIDsExhaustedError):
|
|
293
|
+
uuid.uuid4()
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Exhaustion behaviors:
|
|
297
|
+
- `"cycle"` (default): Loop back to the start of the sequence
|
|
298
|
+
- `"random"`: Fall back to generating random UUIDs
|
|
299
|
+
- `"raise"`: Raise `UUIDsExhaustedError`
|
|
300
|
+
|
|
301
|
+
### Global Configuration
|
|
302
|
+
|
|
303
|
+
Configure default behavior for all tests via `pyproject.toml`:
|
|
304
|
+
|
|
305
|
+
```toml
|
|
306
|
+
# pyproject.toml
|
|
307
|
+
[tool.pytest_uuid]
|
|
308
|
+
default_ignore_list = ["sqlalchemy", "celery"]
|
|
309
|
+
extend_ignore_list = ["myapp.internal"]
|
|
310
|
+
default_exhaustion_behavior = "raise"
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Or programmatically in `conftest.py`:
|
|
314
|
+
|
|
315
|
+
```python
|
|
316
|
+
# conftest.py
|
|
317
|
+
import pytest_uuid
|
|
318
|
+
|
|
319
|
+
pytest_uuid.configure(
|
|
320
|
+
default_ignore_list=["sqlalchemy", "celery"],
|
|
321
|
+
extend_ignore_list=["myapp.internal"],
|
|
322
|
+
default_exhaustion_behavior="raise",
|
|
323
|
+
)
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### Module-Specific Mocking
|
|
327
|
+
|
|
328
|
+
For granular control, use `mock_uuid_factory`:
|
|
329
|
+
|
|
330
|
+
```python
|
|
331
|
+
# myapp/models.py
|
|
332
|
+
from uuid import uuid4
|
|
333
|
+
|
|
334
|
+
def create_user():
|
|
335
|
+
return {"id": str(uuid4()), "name": "John"}
|
|
336
|
+
|
|
337
|
+
# tests/test_models.py
|
|
338
|
+
def test_create_user(mock_uuid_factory):
|
|
339
|
+
with mock_uuid_factory("myapp.models") as mocker:
|
|
340
|
+
mocker.set("12345678-1234-5678-1234-567812345678")
|
|
341
|
+
user = create_user()
|
|
342
|
+
assert user["id"] == "12345678-1234-5678-1234-567812345678"
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### Context Manager
|
|
346
|
+
|
|
347
|
+
Use `freeze_uuid` as a context manager:
|
|
348
|
+
|
|
349
|
+
```python
|
|
350
|
+
from pytest_uuid import freeze_uuid
|
|
351
|
+
|
|
352
|
+
def test_context_manager():
|
|
353
|
+
with freeze_uuid("12345678-1234-5678-1234-567812345678"):
|
|
354
|
+
assert str(uuid.uuid4()) == "12345678-1234-5678-1234-567812345678"
|
|
355
|
+
|
|
356
|
+
# Original uuid.uuid4 is restored
|
|
357
|
+
assert uuid.uuid4() != uuid.UUID("12345678-1234-5678-1234-567812345678")
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### Bring Your Own Randomizer
|
|
361
|
+
|
|
362
|
+
Pass a `random.Random` instance for full control:
|
|
363
|
+
|
|
364
|
+
```python
|
|
365
|
+
import random
|
|
366
|
+
from pytest_uuid import freeze_uuid
|
|
367
|
+
|
|
368
|
+
rng = random.Random(42)
|
|
369
|
+
rng.random() # Advance the state
|
|
370
|
+
|
|
371
|
+
@freeze_uuid(seed=rng)
|
|
372
|
+
def test_custom_rng():
|
|
373
|
+
# Gets UUIDs from the pre-advanced random state
|
|
374
|
+
result = uuid.uuid4()
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### Scoped Mocking
|
|
378
|
+
|
|
379
|
+
#### Module-Level
|
|
380
|
+
|
|
381
|
+
Apply to all tests in a module using pytest's `pytestmark`:
|
|
382
|
+
|
|
383
|
+
```python
|
|
384
|
+
# tests/test_user_creation.py
|
|
385
|
+
import uuid
|
|
386
|
+
import pytest
|
|
387
|
+
|
|
388
|
+
pytestmark = pytest.mark.freeze_uuid("12345678-1234-5678-1234-567812345678")
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def test_create_user():
|
|
392
|
+
assert str(uuid.uuid4()) == "12345678-1234-5678-1234-567812345678"
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def test_create_another_user():
|
|
396
|
+
assert str(uuid.uuid4()) == "12345678-1234-5678-1234-567812345678"
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
#### Class-Level
|
|
400
|
+
|
|
401
|
+
Apply the decorator to a test class to freeze UUIDs for all test methods:
|
|
402
|
+
|
|
403
|
+
```python
|
|
404
|
+
import uuid
|
|
405
|
+
from pytest_uuid import freeze_uuid
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
@freeze_uuid("12345678-1234-5678-1234-567812345678")
|
|
409
|
+
class TestUserService:
|
|
410
|
+
def test_create(self):
|
|
411
|
+
assert str(uuid.uuid4()) == "12345678-1234-5678-1234-567812345678"
|
|
412
|
+
|
|
413
|
+
def test_update(self):
|
|
414
|
+
assert str(uuid.uuid4()) == "12345678-1234-5678-1234-567812345678"
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
Or use the marker:
|
|
418
|
+
|
|
419
|
+
```python
|
|
420
|
+
import uuid
|
|
421
|
+
import pytest
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
@pytest.mark.freeze_uuid(seed=42)
|
|
425
|
+
class TestSeededService:
|
|
426
|
+
def test_one(self):
|
|
427
|
+
result = uuid.uuid4()
|
|
428
|
+
assert result.version == 4
|
|
429
|
+
|
|
430
|
+
def test_two(self):
|
|
431
|
+
result = uuid.uuid4()
|
|
432
|
+
assert result.version == 4
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
#### Session-Level
|
|
436
|
+
|
|
437
|
+
For session-wide mocking, use a session-scoped autouse fixture in `conftest.py`:
|
|
438
|
+
|
|
439
|
+
```python
|
|
440
|
+
# conftest.py
|
|
441
|
+
import pytest
|
|
442
|
+
from pytest_uuid import freeze_uuid
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
@pytest.fixture(scope="session", autouse=True)
|
|
446
|
+
def freeze_uuids_globally():
|
|
447
|
+
with freeze_uuid(seed=12345):
|
|
448
|
+
yield
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
## API Reference
|
|
452
|
+
|
|
453
|
+
### Fixtures
|
|
454
|
+
|
|
455
|
+
#### `mock_uuid`
|
|
456
|
+
|
|
457
|
+
Main fixture for controlling `uuid.uuid4()` calls.
|
|
458
|
+
|
|
459
|
+
**Methods:**
|
|
460
|
+
- `set(*uuids)` - Set one or more UUIDs to return (cycles by default)
|
|
461
|
+
- `set_default(uuid)` - Set a default UUID for all calls
|
|
462
|
+
- `set_seed(seed)` - Set a seed for reproducible generation
|
|
463
|
+
- `set_seed_from_node()` - Use test node ID as seed
|
|
464
|
+
- `set_exhaustion_behavior(behavior)` - Set behavior when sequence exhausted
|
|
465
|
+
- `spy()` - Switch to spy mode (return real UUIDs while still tracking)
|
|
466
|
+
- `reset()` - Reset to initial state
|
|
467
|
+
|
|
468
|
+
#### `mock_uuid_factory`
|
|
469
|
+
|
|
470
|
+
Factory for module-specific mocking.
|
|
471
|
+
|
|
472
|
+
```python
|
|
473
|
+
with mock_uuid_factory("module.path") as mocker:
|
|
474
|
+
mocker.set("...")
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
#### `spy_uuid`
|
|
478
|
+
|
|
479
|
+
Spy fixture that tracks `uuid.uuid4()` calls without mocking them.
|
|
480
|
+
|
|
481
|
+
```python
|
|
482
|
+
def test_spy(spy_uuid):
|
|
483
|
+
result = uuid.uuid4() # Returns real random UUID
|
|
484
|
+
|
|
485
|
+
assert spy_uuid.call_count == 1
|
|
486
|
+
assert spy_uuid.last_uuid == result
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
**Properties:**
|
|
490
|
+
- `call_count` - Number of times uuid4 was called
|
|
491
|
+
- `generated_uuids` - List of all generated UUIDs
|
|
492
|
+
- `last_uuid` - Most recently generated UUID
|
|
493
|
+
- `calls` - List of `UUIDCall` records with metadata
|
|
494
|
+
|
|
495
|
+
**Methods:**
|
|
496
|
+
- `reset()` - Reset tracking data
|
|
497
|
+
- `calls_from(module_prefix)` - Filter calls by module prefix
|
|
498
|
+
|
|
499
|
+
### Call Tracking
|
|
500
|
+
|
|
501
|
+
Both `mock_uuid` and `spy_uuid` fixtures provide detailed call tracking via the `UUIDCall` dataclass:
|
|
502
|
+
|
|
503
|
+
```python
|
|
504
|
+
from pytest_uuid.types import UUIDCall
|
|
505
|
+
|
|
506
|
+
def test_call_tracking(mock_uuid):
|
|
507
|
+
mock_uuid.set("12345678-1234-5678-1234-567812345678")
|
|
508
|
+
uuid.uuid4()
|
|
509
|
+
|
|
510
|
+
call = mock_uuid.calls[0]
|
|
511
|
+
assert call.uuid == uuid.UUID("12345678-1234-5678-1234-567812345678")
|
|
512
|
+
assert call.was_mocked is True
|
|
513
|
+
assert call.caller_module is not None
|
|
514
|
+
assert call.caller_file is not None
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
**`UUIDCall` Fields:**
|
|
518
|
+
- `uuid` - The UUID that was returned
|
|
519
|
+
- `was_mocked` - `True` if mocked, `False` if real (spy mode or ignored module)
|
|
520
|
+
- `caller_module` - Name of the module that made the call
|
|
521
|
+
- `caller_file` - File path where the call originated
|
|
522
|
+
|
|
523
|
+
**Additional `mock_uuid` Properties:**
|
|
524
|
+
- `calls` - All call records
|
|
525
|
+
- `mocked_calls` - Only calls that returned mocked UUIDs
|
|
526
|
+
- `real_calls` - Only calls that returned real UUIDs (spy mode)
|
|
527
|
+
- `mocked_count` - Number of mocked calls
|
|
528
|
+
- `real_count` - Number of real calls
|
|
529
|
+
|
|
530
|
+
#### Filtering Calls by Module
|
|
531
|
+
|
|
532
|
+
```python
|
|
533
|
+
def test_filter_calls(mock_uuid):
|
|
534
|
+
mock_uuid.set("12345678-1234-5678-1234-567812345678")
|
|
535
|
+
|
|
536
|
+
uuid.uuid4() # Call from test module
|
|
537
|
+
mymodule.do_something() # Calls uuid4 internally
|
|
538
|
+
|
|
539
|
+
# Filter calls by module prefix
|
|
540
|
+
test_calls = mock_uuid.calls_from("tests")
|
|
541
|
+
module_calls = mock_uuid.calls_from("mymodule")
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
### Decorator/Context Manager
|
|
545
|
+
|
|
546
|
+
#### `freeze_uuid`
|
|
547
|
+
|
|
548
|
+
```python
|
|
549
|
+
from pytest_uuid import freeze_uuid
|
|
550
|
+
|
|
551
|
+
# Static UUID
|
|
552
|
+
@freeze_uuid("12345678-1234-5678-1234-567812345678")
|
|
553
|
+
def test_static(): ...
|
|
554
|
+
|
|
555
|
+
# Sequence
|
|
556
|
+
@freeze_uuid(["uuid1", "uuid2"], on_exhausted="raise")
|
|
557
|
+
def test_sequence(): ...
|
|
558
|
+
|
|
559
|
+
# Seeded
|
|
560
|
+
@freeze_uuid(seed=42)
|
|
561
|
+
def test_seeded(): ...
|
|
562
|
+
|
|
563
|
+
# Node-seeded (for use with marker)
|
|
564
|
+
@pytest.mark.freeze_uuid(seed="node")
|
|
565
|
+
def test_node_seeded(): ...
|
|
566
|
+
|
|
567
|
+
# Context manager
|
|
568
|
+
with freeze_uuid("...") as freezer:
|
|
569
|
+
result = uuid.uuid4()
|
|
570
|
+
freezer.reset()
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
**Parameters:**
|
|
574
|
+
- `uuids` - UUID(s) to return (string, UUID, or sequence)
|
|
575
|
+
- `seed` - Integer, `random.Random`, or `"node"` for reproducible generation
|
|
576
|
+
- `on_exhausted` - `"cycle"`, `"random"`, or `"raise"`
|
|
577
|
+
- `ignore` - Module prefixes to exclude from patching
|
|
578
|
+
|
|
579
|
+
### Marker
|
|
580
|
+
|
|
581
|
+
```python
|
|
582
|
+
@pytest.mark.freeze_uuid("uuid")
|
|
583
|
+
@pytest.mark.freeze_uuid(["uuid1", "uuid2"])
|
|
584
|
+
@pytest.mark.freeze_uuid(seed=42)
|
|
585
|
+
@pytest.mark.freeze_uuid(seed="node")
|
|
586
|
+
@pytest.mark.freeze_uuid("uuid", on_exhausted="raise")
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
### Configuration
|
|
590
|
+
|
|
591
|
+
```python
|
|
592
|
+
import pytest_uuid
|
|
593
|
+
|
|
594
|
+
pytest_uuid.configure(
|
|
595
|
+
default_ignore_list=["package1", "package2"],
|
|
596
|
+
extend_ignore_list=["package3"],
|
|
597
|
+
default_exhaustion_behavior="raise",
|
|
598
|
+
)
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
### References
|
|
602
|
+
|
|
603
|
+
- [RFC 9562 - UUID Specification](https://datatracker.ietf.org/doc/html/rfc9562)
|
|
604
|
+
|
|
605
|
+
## Development
|
|
606
|
+
|
|
607
|
+
This project uses [uv](https://docs.astral.sh/uv/) for package management and [just](https://just.systems/) as a command runner.
|
|
608
|
+
|
|
609
|
+
### Prerequisites
|
|
610
|
+
|
|
611
|
+
```bash
|
|
612
|
+
# Install uv (if not already installed)
|
|
613
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
614
|
+
|
|
615
|
+
# Install just (macOS)
|
|
616
|
+
brew install just
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
### Setup
|
|
620
|
+
|
|
621
|
+
```bash
|
|
622
|
+
git clone https://github.com/CaptainDriftwood/pytest-uuid.git
|
|
623
|
+
cd pytest-uuid
|
|
624
|
+
just sync
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
### Available Commands
|
|
628
|
+
|
|
629
|
+
```bash
|
|
630
|
+
just # List all commands
|
|
631
|
+
just test # Run tests
|
|
632
|
+
just test-cov # Run tests with coverage
|
|
633
|
+
just nox # Run tests across all Python versions with nox
|
|
634
|
+
just nox 3.12 # Run tests for a specific Python version
|
|
635
|
+
just lint # Run linting
|
|
636
|
+
just format # Format code
|
|
637
|
+
just check # Run all checks
|
|
638
|
+
just build # Build the package
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
### Coverage with Pytester
|
|
642
|
+
|
|
643
|
+
This project uses [pytester](https://docs.pytest.org/en/stable/reference/reference.html#pytester) for integration testing. Getting accurate coverage for pytest plugins requires special handling because plugins are imported before coverage can start measuring.
|
|
644
|
+
|
|
645
|
+
**The Problem:**
|
|
646
|
+
|
|
647
|
+
When running `pytest --cov=pytest_uuid`, the plugin is loaded when pytest starts—*before* pytest-cov begins measuring. This causes incomplete coverage and the warning:
|
|
648
|
+
|
|
649
|
+
```
|
|
650
|
+
CoverageWarning: Module pytest_uuid was previously imported, but not measured
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
**The Solution:**
|
|
654
|
+
|
|
655
|
+
Use `coverage run -m pytest` instead of `pytest --cov`:
|
|
656
|
+
|
|
657
|
+
```bash
|
|
658
|
+
# Instead of this:
|
|
659
|
+
pytest --cov=pytest_uuid --cov-report=term-missing
|
|
660
|
+
|
|
661
|
+
# Use this:
|
|
662
|
+
coverage run -m pytest
|
|
663
|
+
coverage combine
|
|
664
|
+
coverage report --show-missing
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
This works because `coverage run` starts measuring *before* Python imports anything, so the plugin import is captured.
|
|
668
|
+
|
|
669
|
+
**Configuration (`pyproject.toml`):**
|
|
670
|
+
|
|
671
|
+
```toml
|
|
672
|
+
[tool.coverage.run]
|
|
673
|
+
source = ["src/pytest_uuid"]
|
|
674
|
+
branch = true
|
|
675
|
+
parallel = true # Required for combining coverage files
|
|
676
|
+
patch = ["subprocess"] # Enables coverage in subprocesses
|
|
677
|
+
sigterm = true # Ensures coverage is saved on SIGTERM
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
**Why `parallel = true`?**
|
|
681
|
+
|
|
682
|
+
When coverage patches subprocesses, each subprocess writes its own `.coverage.<hostname>.<pid>.<random>` file. The `coverage combine` command merges these into a single `.coverage` file for reporting.
|
|
683
|
+
|
|
684
|
+
**References:**
|
|
685
|
+
|
|
686
|
+
- [pytest-cov Subprocess Support](https://pytest-cov.readthedocs.io/en/latest/subprocess-support.html)
|
|
687
|
+
- [coverage.py Subprocess Measurement](https://coverage.readthedocs.io/en/latest/subprocess.html)
|
|
688
|
+
- [pytest-cov Issue #587 - Plugin Coverage](https://github.com/pytest-dev/pytest-cov/issues/587)
|
|
689
|
+
|
|
690
|
+
## License
|
|
691
|
+
|
|
692
|
+
MIT License - see [LICENSE](LICENSE) for details.
|