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.
@@ -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
+ [![PyPI version](https://img.shields.io/pypi/v/pytest-uuid.svg)](https://pypi.org/project/pytest-uuid/)
43
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
44
+ [![Tests](https://github.com/CaptainDriftwood/pytest-uuid/actions/workflows/test.yml/badge.svg)](https://github.com/CaptainDriftwood/pytest-uuid/actions/workflows/test.yml)
45
+ [![codecov](https://codecov.io/gh/CaptainDriftwood/pytest-uuid/graph/badge.svg)](https://codecov.io/gh/CaptainDriftwood/pytest-uuid)
46
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
47
+ [![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)
48
+ [![ty](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ty/main/assets/badge/v0.json)](https://github.com/astral-sh/ty)
49
+ [![pytest](https://img.shields.io/badge/pytest-plugin-blue.svg)](https://docs.pytest.org/)
50
+
51
+ ![Python](https://img.shields.io/badge/python-3.9%20|%203.10%20|%203.11%20|%203.12%20|%203.13%20|%203.14-blue.svg)
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.