A pytest plugin for mocking uuid.uuid4() calls
Project description
pytest-uuid
A pytest plugin for mocking UUID generation in your tests. Supports uuid1, uuid3, uuid4, uuid5, uuid6, uuid7, and uuid8.
Features
- Mock all UUID versions: uuid1, uuid3, uuid4, uuid5, uuid6, uuid7, uuid8
- Works with both
import uuidandfrom uuid import uuid4patterns (how?) - Multiple ways to mock: static, sequence, seeded, or node-seeded
- Decorator, marker, and fixture APIs (inspired by freezegun)
- Configurable exhaustion behavior for sequences
- Ignore list for packages that should use real UUIDs
- Spy mode to track calls without mocking
- Detailed call tracking with caller module/file info
- Automatic cleanup after each test
- Zero configuration required - just use the fixture
- uuid6/uuid7/uuid8 support via uuid6 backport (Python < 3.14)
Installation
uv add --group dev pytest-uuid
Or with pip:
pip install pytest-uuid
Quick Start
Fixture API
import uuid
def test_single_uuid(mock_uuid):
mock_uuid.uuid4.set("12345678-1234-4678-8234-567812345678")
assert str(uuid.uuid4()) == "12345678-1234-4678-8234-567812345678"
def test_multiple_uuids(mock_uuid):
mock_uuid.uuid4.set(
"11111111-1111-4111-8111-111111111111",
"22222222-2222-4222-8222-222222222222",
)
assert str(uuid.uuid4()) == "11111111-1111-4111-8111-111111111111"
assert str(uuid.uuid4()) == "22222222-2222-4222-8222-222222222222"
# Cycles back to the first UUID
assert str(uuid.uuid4()) == "11111111-1111-4111-8111-111111111111"
Decorator API
import uuid
from pytest_uuid import freeze_uuid4, freeze_uuid1, freeze_uuid7
@freeze_uuid4("12345678-1234-4678-8234-567812345678")
def test_with_decorator():
assert str(uuid.uuid4()) == "12345678-1234-4678-8234-567812345678"
@freeze_uuid4(seed=42)
def test_seeded():
# Reproducible UUIDs from seed
result = uuid.uuid4()
assert result.version == 4
# Stack multiple versions
@freeze_uuid4("44444444-4444-4444-8444-444444444444")
@freeze_uuid1("11111111-1111-1111-8111-111111111111")
def test_multiple_versions():
assert str(uuid.uuid4()) == "44444444-4444-4444-8444-444444444444"
assert str(uuid.uuid1()) == "11111111-1111-1111-8111-111111111111"
Backward compatibility: The generic
freeze_uuidis still available as an alias forfreeze_uuid4.
Marker API
import uuid
import pytest
@pytest.mark.freeze_uuid4("12345678-1234-4678-8234-567812345678")
def test_with_marker():
assert str(uuid.uuid4()) == "12345678-1234-4678-8234-567812345678"
@pytest.mark.freeze_uuid4(seed="node")
def test_node_seeded():
# Same test always gets the same UUIDs
result = uuid.uuid4()
assert result.version == 4
# Stack multiple version markers
@pytest.mark.freeze_uuid4("44444444-4444-4444-8444-444444444444")
@pytest.mark.freeze_uuid1("11111111-1111-1111-8111-111111111111")
def test_multiple_versions():
assert str(uuid.uuid4()) == "44444444-4444-4444-8444-444444444444"
assert str(uuid.uuid1()) == "11111111-1111-1111-8111-111111111111"
Backward compatibility: The generic
freeze_uuidmarker is still available as an alias forfreeze_uuid4.
Usage
Static UUIDs
Return the same UUID every time:
def test_static(mock_uuid):
mock_uuid.uuid4.set("12345678-1234-4678-8234-567812345678")
assert uuid.uuid4() == uuid.uuid4() # Same UUID
# Or with decorator
@freeze_uuid4("12345678-1234-4678-8234-567812345678")
def test_static_decorator():
assert uuid.uuid4() == uuid.uuid4() # Same UUID
UUID Sequences
Return UUIDs from a list:
def test_sequence(mock_uuid):
mock_uuid.uuid4.set(
"11111111-1111-4111-8111-111111111111",
"22222222-2222-4222-8222-222222222222",
)
assert str(uuid.uuid4()) == "11111111-1111-4111-8111-111111111111"
assert str(uuid.uuid4()) == "22222222-2222-4222-8222-222222222222"
# Cycles back by default
assert str(uuid.uuid4()) == "11111111-1111-4111-8111-111111111111"
Seeded UUIDs
Generate reproducible UUIDs from a seed:
def test_seeded(mock_uuid):
mock_uuid.uuid4.set_seed(42)
first = uuid.uuid4()
mock_uuid.uuid4.set_seed(42) # Reset to same seed
assert uuid.uuid4() == first # Same UUID
# With decorator
@freeze_uuid4(seed=42)
def test_seeded_decorator():
result = uuid.uuid4()
assert result.version == 4 # Valid UUID v4
Node-Seeded UUIDs (Recommended)
Derive the seed from the test's node ID for automatic reproducibility:
def test_node_seeded(mock_uuid):
mock_uuid.uuid4.set_seed_from_node()
# Same test always produces the same sequence
# With marker
@pytest.mark.freeze_uuid4(seed="node")
def test_node_seeded_marker():
# Same test always produces the same sequence
pass
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.
Class-Level Node Seeding
import uuid
import pytest
@pytest.mark.freeze_uuid4(seed="node")
class TestUserService:
def test_create(self):
# Seed derived from "test_module.py::TestUserService::test_create"
result = uuid.uuid4()
assert result.version == 4
def test_update(self):
# Seed derived from "test_module.py::TestUserService::test_update"
result = uuid.uuid4()
assert result.version == 4
Module-Level Node Seeding
# tests/test_user_creation.py
import uuid
import pytest
pytestmark = pytest.mark.freeze_uuid4(seed="node")
def test_create_user():
# Seed derived from "test_user_creation.py::test_create_user"
result = uuid.uuid4()
assert result.version == 4
def test_create_admin():
# Seed derived from "test_user_creation.py::test_create_admin"
result = uuid.uuid4()
assert result.version == 4
Function-Level Node Seeding (Autouse Fixture)
For automatic node seeding on every test without markers:
# conftest.py
import pytest
@pytest.fixture(autouse=True)
def auto_seed_uuids(mock_uuid):
"""Automatically seed uuid4 from test node ID for all tests."""
mock_uuid.uuid4.set_seed_from_node()
Every test now gets reproducible UUIDs without any boilerplate:
# test_example.py
import uuid
def test_user_creation():
# No fixture argument needed - autouse handles it
# UUIDs are deterministic based on this test's node ID
user_id = uuid.uuid4()
assert user_id.version == 4
def test_order_creation():
# Different test = different seed = different UUIDs
order_id = uuid.uuid4()
assert order_id.version == 4
Session-Level Node Seeding
# conftest.py
import hashlib
import pytest
from pytest_uuid import freeze_uuid4
@pytest.fixture(scope="session", autouse=True)
def freeze_uuids_globally(request):
# Use hashlib for deterministic seeding across processes.
# Python's hash() is randomized per-process via PYTHONHASHSEED:
# https://docs.python.org/3/using/cmdline.html#envvar-PYTHONHASHSEED
#
# Convert node ID to a deterministic integer seed:
# 1. hashlib.sha256() creates a hash of the node ID string
# 2. .hexdigest() returns the hash as a 64-char hex string
# 3. [:16] takes first 16 hex chars (64 bits) - plenty of uniqueness
# 4. int(..., 16) converts hex string to integer
node_bytes = request.node.nodeid.encode()
seed = int(hashlib.sha256(node_bytes).hexdigest()[:16], 16)
with freeze_uuid4(seed=seed):
yield
Note: For session-level fixtures, use
request.node.nodeiddirectly sinceseed="node"in the marker requires per-test context. Alternatively, use a fixed seed for true global determinism. Always usehashlib(nothash()) for node-derived seeds, as Python's built-inhash()is randomized per-process.
Exhaustion Behavior
Control what happens when a UUID sequence is exhausted:
from pytest_uuid import ExhaustionBehavior, UUIDsExhaustedError
def test_exhaustion_raise(mock_uuid):
mock_uuid.uuid4.set_exhaustion_behavior("raise")
mock_uuid.uuid4.set("11111111-1111-4111-8111-111111111111")
uuid.uuid4() # Returns the UUID
with pytest.raises(UUIDsExhaustedError):
uuid.uuid4() # Raises - sequence exhausted
# With decorator
@freeze_uuid4(
["11111111-1111-4111-8111-111111111111"],
on_exhausted="raise", # or "cycle" or "random"
)
def test_exhaustion_decorator():
uuid.uuid4()
with pytest.raises(UUIDsExhaustedError):
uuid.uuid4()
Exhaustion behaviors:
"cycle"(default): Loop back to the start of the sequence"random": Fall back to generating random UUIDs"raise": RaiseUUIDsExhaustedError
Spy Mode
Track uuid.uuid4() calls without mocking them. Useful when you need to verify UUID generation happens without controlling the output.
Using spy_uuid Fixture
# myapp/models.py
from uuid import uuid4
class User:
def __init__(self, name):
self.id = str(uuid4())
self.name = name
# tests/test_models.py
def test_user_generates_uuid(spy_uuid):
"""Verify User creates a UUID without controlling its value."""
user = User("Alice")
assert spy_uuid.call_count == 1
assert user.id == str(spy_uuid.last_uuid)
Using mock_uuid.uuid4.spy()
Switch from mocked to real UUIDs mid-test:
def test_start_mocked_then_spy(mock_uuid):
"""Start with mocked UUIDs, then switch to real ones."""
mock_uuid.uuid4.set("12345678-1234-4678-8234-567812345678")
first = uuid.uuid4() # Mocked
mock_uuid.uuid4.spy() # Switch to spy mode
second = uuid.uuid4() # Real random UUID
assert str(first) == "12345678-1234-4678-8234-567812345678"
assert first != second # second is random
assert mock_uuid.uuid4.mocked_count == 1
assert mock_uuid.uuid4.real_count == 1
When to use which: Use
spy_uuidwhen you never need mocking in the test. Usemock_uuid.uuid4.spy()when you need to switch between mocked and real UUIDs within the same test.
Additional UUID Versions
In addition to uuid4, pytest-uuid supports mocking all UUID versions:
UUID1 (Time-based with MAC)
def test_uuid1(mock_uuid):
mock_uuid.uuid1.set("12345678-1234-1234-8234-567812345678")
assert str(uuid.uuid1()) == "12345678-1234-1234-8234-567812345678"
def test_uuid1_seeded(mock_uuid):
mock_uuid.uuid1.set_seed(42)
first = uuid.uuid1()
mock_uuid.uuid1.reset()
mock_uuid.uuid1.set_seed(42)
assert uuid.uuid1() == first
def test_uuid1_fixed_node(mock_uuid):
# Return real uuid1 values with a fixed node (MAC address)
mock_uuid.uuid1.set_node(0x123456789ABC)
result = uuid.uuid1()
assert result.node == 0x123456789ABC
UUID3 and UUID5 (Namespace-based)
UUID3 (MD5) and UUID5 (SHA-1) are deterministic - the same namespace and name always produce the same UUID. These functions are tracked in spy mode only:
def test_uuid3_tracking(mock_uuid):
_ = mock_uuid.uuid3 # Initialize spy
result = uuid.uuid3(uuid.NAMESPACE_DNS, "example.com")
assert mock_uuid.uuid3.call_count == 1
assert mock_uuid.uuid3.calls[0].namespace == uuid.NAMESPACE_DNS
assert mock_uuid.uuid3.calls[0].name == "example.com"
def test_uuid5_filtering(mock_uuid):
_ = mock_uuid.uuid5
uuid.uuid5(uuid.NAMESPACE_DNS, "example.com")
uuid.uuid5(uuid.NAMESPACE_URL, "https://example.com")
dns_calls = mock_uuid.uuid5.calls_with_namespace(uuid.NAMESPACE_DNS)
assert len(dns_calls) == 1
UUID6, UUID7, UUID8 (RFC 9562)
These newer UUID versions require Python 3.14+ or the uuid6 backport package:
from uuid6 import uuid6, uuid7, uuid8 # or from uuid import ... on Python 3.14+
def test_uuid7(mock_uuid):
mock_uuid.uuid7.set("12345678-1234-7234-8234-567812345678")
result = uuid7()
assert str(result) == "12345678-1234-7234-8234-567812345678"
def test_uuid7_seeded(mock_uuid):
mock_uuid.uuid7.set_seed(42)
first = uuid7()
mock_uuid.uuid7.reset()
mock_uuid.uuid7.set_seed(42)
assert uuid7() == first
def test_all_versions_independent(mock_uuid):
"""Each UUID version is mocked independently."""
mock_uuid.uuid4.set("44444444-4444-4444-8444-444444444444") # uuid4
mock_uuid.uuid1.set("11111111-1111-1111-8111-111111111111")
mock_uuid.uuid7.set("77777777-7777-7777-8777-777777777777")
assert str(uuid.uuid4()) == "44444444-4444-4444-8444-444444444444"
assert str(uuid.uuid1()) == "11111111-1111-1111-8111-111111111111"
assert str(uuid7()) == "77777777-7777-7777-8777-777777777777"
# Call counts are tracked independently
assert mock_uuid.uuid4.call_count == 1 # uuid4
assert mock_uuid.uuid1.call_count == 1
assert mock_uuid.uuid7.call_count == 1
Note: uuid6/uuid7/uuid8 require the
uuid6package on Python < 3.14. Install withpip install uuid6or it will be installed automatically as a dependency.
Ignoring Modules
Exclude specific packages from UUID mocking so they receive real UUIDs. This is useful for third-party libraries like SQLAlchemy or Celery that need real UUIDs for internal operations.
Fixture API
def test_with_ignored_modules(mock_uuid):
mock_uuid.uuid4.set("12345678-1234-4678-8234-567812345678")
mock_uuid.uuid4.set_ignore("sqlalchemy", "celery")
# Direct calls are mocked
assert str(uuid.uuid4()) == "12345678-1234-4678-8234-567812345678"
# Calls from sqlalchemy/celery get real UUIDs
# (the ignore check walks the entire call stack)
Decorator/Marker API
@freeze_uuid4("12345678-1234-4678-8234-567812345678", ignore=["sqlalchemy"])
def test_with_decorator():
assert str(uuid.uuid4()) == "12345678-1234-4678-8234-567812345678"
@pytest.mark.freeze_uuid4("...", ignore=["celery"])
def test_with_marker():
pass
How it works: The ignore check inspects the entire call stack, not just the immediate caller. If any frame in the call chain is from an ignored module, real UUIDs are returned. This handles cases where your code calls a library that internally calls
uuid.uuid4().
Tracking Ignored Calls
def test_tracking(mock_uuid):
mock_uuid.uuid4.set("12345678-1234-4678-8234-567812345678")
mock_uuid.uuid4.set_ignore("mylib")
uuid.uuid4() # mocked
mylib.create_record() # real (from ignored module)
assert mock_uuid.uuid4.mocked_count == 1
assert mock_uuid.uuid4.real_count == 1
Global Configuration
Configure default behavior for all tests via pyproject.toml:
# pyproject.toml
[tool.pytest_uuid]
default_ignore_list = ["sqlalchemy", "celery"]
extend_ignore_list = ["myapp.internal"]
default_exhaustion_behavior = "raise"
Or programmatically in conftest.py:
# conftest.py
import pytest_uuid
pytest_uuid.configure(
default_ignore_list=["sqlalchemy", "celery"],
extend_ignore_list=["myapp.internal"],
default_exhaustion_behavior="raise",
)
Default Ignore List: By default,
botocoreis in the ignore list. This prevents pytest-uuid from interfering with AWS SDK operations that useuuid.uuid4()internally for idempotency tokens. Useextend_ignore_listto add more packages, or setdefault_ignore_listto override completely.
Module-Specific Mocking
For granular control, use mock_uuid_factory:
# myapp/models.py
from uuid import uuid4
def create_user():
return {"id": str(uuid4()), "name": "John"}
# tests/test_models.py
def test_create_user(mock_uuid_factory):
with mock_uuid_factory("myapp.models") as mocker:
mocker.uuid4.set("12345678-1234-4678-8234-567812345678")
user = create_user()
assert user["id"] == "12345678-1234-4678-8234-567812345678"
Context Manager
Use freeze functions as context managers:
from pytest_uuid import freeze_uuid4
def test_context_manager():
with freeze_uuid4("12345678-1234-4678-8234-567812345678"):
assert str(uuid.uuid4()) == "12345678-1234-4678-8234-567812345678"
# Original uuid.uuid4 is restored
assert uuid.uuid4() != uuid.UUID("12345678-1234-4678-8234-567812345678")
Bring Your Own Randomizer
Pass a random.Random instance for full control:
import random
from pytest_uuid import freeze_uuid4
rng = random.Random(42)
rng.random() # Advance the state
@freeze_uuid4(seed=rng)
def test_custom_rng():
# Gets UUIDs from the pre-advanced random state
result = uuid.uuid4()
Scoped Mocking
Module-Level
Apply to all tests in a module using pytest's pytestmark:
# tests/test_user_creation.py
import uuid
import pytest
pytestmark = pytest.mark.freeze_uuid4("12345678-1234-4678-8234-567812345678")
def test_create_user():
assert str(uuid.uuid4()) == "12345678-1234-4678-8234-567812345678"
def test_create_another_user():
assert str(uuid.uuid4()) == "12345678-1234-4678-8234-567812345678"
Class-Level
Apply the decorator to a test class to freeze UUIDs for all test methods:
import uuid
from pytest_uuid import freeze_uuid4
@freeze_uuid4("12345678-1234-4678-8234-567812345678")
class TestUserService:
def test_create(self):
assert str(uuid.uuid4()) == "12345678-1234-4678-8234-567812345678"
def test_update(self):
assert str(uuid.uuid4()) == "12345678-1234-4678-8234-567812345678"
Or use the marker:
import uuid
import pytest
@pytest.mark.freeze_uuid4(seed=42)
class TestSeededService:
def test_one(self):
result = uuid.uuid4()
assert result.version == 4
def test_two(self):
result = uuid.uuid4()
assert result.version == 4
Session-Level
For session-wide mocking, use a session-scoped autouse fixture in conftest.py:
# conftest.py
import pytest
from pytest_uuid import freeze_uuid4
@pytest.fixture(scope="session", autouse=True)
def freeze_uuids_globally():
with freeze_uuid4(seed=12345):
yield
API Reference
Fixtures
mock_uuid
Container fixture providing access to mockers for all UUID versions.
Properties (version-specific mockers):
mock_uuid.uuid4- UUID4Mocker foruuid.uuid4()(see methods below)mock_uuid.uuid1- UUID1Mocker foruuid.uuid1()withset(),set_seed(),set_node(),set_clock_seq()mock_uuid.uuid3- NamespaceUUIDSpy foruuid.uuid3()(spy-only, tracks namespace/name)mock_uuid.uuid5- NamespaceUUIDSpy foruuid.uuid5()(spy-only, tracks namespace/name)mock_uuid.uuid6- UUID6Mocker foruuid.uuid6()withset(),set_seed(),set_node()(requires uuid6 package)mock_uuid.uuid7- UUID7Mocker foruuid.uuid7()withset(),set_seed()(requires uuid6 package)mock_uuid.uuid8- UUID8Mocker foruuid.uuid8()withset(),set_seed()(requires uuid6 package)
Container Methods:
reset()- Reset all initialized sub-mockers to initial state
UUID4Mocker Methods (accessed via mock_uuid.uuid4):
set(*uuids)- Set one or more UUIDs to return (cycles by default)set_default(uuid)- Set a default UUID for all callsset_seed(seed)- Set a seed for reproducible generationset_seed_from_node()- Use test node ID as seedset_exhaustion_behavior(behavior)- Set behavior when sequence exhaustedspy()- Switch to spy mode (return real UUIDs while still tracking)reset()- Reset to initial stateset_ignore(*module_prefixes)- Set modules to ignore (returns real UUIDs)
mock_uuid_factory
Factory for module-specific mocking.
with mock_uuid_factory("module.path") as mocker:
mocker.uuid4.set("...")
spy_uuid
Spy fixture that tracks uuid.uuid4() calls without mocking them.
def test_spy(spy_uuid):
result = uuid.uuid4() # Returns real random UUID
assert spy_uuid.call_count == 1
assert spy_uuid.last_uuid == result
Properties:
call_count- Number of times uuid4 was calledgenerated_uuids- List of all generated UUIDslast_uuid- Most recently generated UUIDcalls- List ofUUIDCallrecords with metadata
Methods:
reset()- Reset tracking datacalls_from(module_prefix)- Filter calls by module prefix
Call Tracking
Both mock_uuid and spy_uuid fixtures provide detailed call tracking via the UUIDCall dataclass:
from pytest_uuid.types import UUIDCall
def test_call_tracking(mock_uuid):
mock_uuid.uuid4.set("12345678-1234-4678-8234-567812345678")
uuid.uuid4()
call = mock_uuid.uuid4.calls[0]
assert call.uuid == uuid.UUID("12345678-1234-4678-8234-567812345678")
assert call.was_mocked is True
assert call.caller_module is not None
assert call.caller_file is not None
UUIDCall Fields:
uuid- The UUID that was returnedwas_mocked-Trueif mocked,Falseif real (spy mode or ignored module)uuid_version- The UUID version (1, 3, 4, 5, 6, 7, or 8)caller_module- Name of the module that made the callcaller_file- File path where the call originatedcaller_line- Line number of the callcaller_function- Function name where the call originatedcaller_qualname- Qualified name (e.g.,MyClass.methodorouter.<locals>.inner)
Tracking Properties (available on mock_uuid.uuid4 and spy_uuid):
call_count- Total number of uuid4 callsgenerated_uuids- List of all UUIDs returnedlast_uuid- Most recently returned UUIDcalls- List ofUUIDCallrecords with full metadata
Additional mock_uuid.uuid4 Properties:
mocked_calls- Only calls that returned mocked UUIDsreal_calls- Only calls that returned real UUIDs (spy mode or ignored modules)mocked_count- Number of mocked callsreal_count- Number of real calls
Interrogating Multiple Calls
def test_interrogate_calls(mock_uuid):
"""Inspect detailed metadata for all uuid4 calls."""
mock_uuid.uuid4.set(
"aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
)
first = uuid.uuid4()
second = uuid.uuid4()
# Check all UUIDs generated
assert len(mock_uuid.uuid4.generated_uuids) == 2
assert mock_uuid.uuid4.generated_uuids[0] == first
assert mock_uuid.uuid4.generated_uuids[1] == second
# Get the last UUID quickly
assert mock_uuid.uuid4.last_uuid == second
# Iterate through call details
for i, call in enumerate(mock_uuid.uuid4.calls):
print(f"Call {i}: {call.uuid}")
print(f" Module: {call.caller_module}")
print(f" File: {call.caller_file}")
print(f" Mocked: {call.was_mocked}")
Distinguishing Mocked vs Real Calls
def test_mixed_mocked_and_real(mock_uuid):
"""Track both mocked calls and real calls from ignored modules."""
mock_uuid.uuid4.set("12345678-1234-4678-8234-567812345678")
mock_uuid.uuid4.set_ignore("mylib")
uuid.uuid4() # Mocked (direct call)
mylib.create_record() # Real (from ignored module)
uuid.uuid4() # Mocked (direct call)
# Count by type
assert mock_uuid.uuid4.call_count == 3
assert mock_uuid.uuid4.mocked_count == 2
assert mock_uuid.uuid4.real_count == 1
# Access only real calls
for call in mock_uuid.uuid4.real_calls:
print(f"Real UUID from {call.caller_module}: {call.uuid}")
# Access only mocked calls
for call in mock_uuid.uuid4.mocked_calls:
assert call.was_mocked is True
Filtering Calls by Module
def test_filter_calls(mock_uuid):
mock_uuid.uuid4.set("12345678-1234-4678-8234-567812345678")
uuid.uuid4() # Call from test module
mymodule.do_something() # Calls uuid4 internally
# Filter calls by module prefix
test_calls = mock_uuid.uuid4.calls_from("tests")
module_calls = mock_uuid.uuid4.calls_from("mymodule")
# Useful for verifying specific modules made expected calls
assert len(module_calls) == 1
Decorator/Context Manager
Version-Specific Freeze Functions
The recommended API uses version-specific functions for clarity:
from pytest_uuid import freeze_uuid4, freeze_uuid1, freeze_uuid6, freeze_uuid7, freeze_uuid8
# uuid4 (most common)
@freeze_uuid4("12345678-1234-4678-8234-567812345678")
def test_static(): ...
@freeze_uuid4(seed=42)
def test_seeded(): ...
# uuid1 with node/clock_seq
@freeze_uuid1(seed=42, node=0x123456789ABC)
def test_uuid1(): ...
# uuid7 (requires uuid6 package or Python 3.14+)
@freeze_uuid7(seed=42)
def test_uuid7(): ...
# Context manager
with freeze_uuid4("...") as freezer:
result = uuid.uuid4()
freezer.reset()
# Stack multiple versions
with freeze_uuid4("..."), freeze_uuid7(seed=42):
uuid.uuid4() # frozen
uuid7() # frozen with seed
Available Functions:
freeze_uuid4- Freezeuuid.uuid4()callsfreeze_uuid1- Freezeuuid.uuid1()calls (supportsnode,clock_seqparameters)freeze_uuid6- Freezeuuid.uuid6()calls (requires uuid6 package, supportsnode,clock_seq)freeze_uuid7- Freezeuuid.uuid7()calls (requires uuid6 package)freeze_uuid8- Freezeuuid.uuid8()calls (requires uuid6 package)freeze_uuid- Backward-compatible alias forfreeze_uuid4
Parameters:
uuids- UUID(s) to return (string, UUID, or sequence)seed- Integer,random.Random, or"node"for reproducible generationon_exhausted-"cycle","random", or"raise"ignore- Module prefixes to exclude from patchingignore_defaults- IfFalse, don't include the default ignore list (default:True)node- (uuid1/uuid6 only) Fixed 48-bit node (MAC address) for seeded generationclock_seq- (uuid1/uuid6 only) Fixed 14-bit clock sequence for seeded generation
Markers
Version-specific markers correspond to the freeze functions:
# uuid4 markers
@pytest.mark.freeze_uuid4("uuid")
@pytest.mark.freeze_uuid4(seed=42)
@pytest.mark.freeze_uuid4(seed="node")
# uuid1 markers
@pytest.mark.freeze_uuid1("uuid")
@pytest.mark.freeze_uuid1(seed=42, node=0x123456789ABC)
# uuid7 markers
@pytest.mark.freeze_uuid7(seed=42)
# Stack multiple version markers
@pytest.mark.freeze_uuid4("44444444-4444-4444-8444-444444444444")
@pytest.mark.freeze_uuid1("11111111-1111-1111-8111-111111111111")
def test_multiple():
uuid.uuid4() # returns 44444444-...
uuid.uuid1() # returns 11111111-...
# Backward-compatible (alias for freeze_uuid4)
@pytest.mark.freeze_uuid("uuid")
Configuration
import pytest_uuid
pytest_uuid.configure(
default_ignore_list=["package1", "package2"],
extend_ignore_list=["package3"],
default_exhaustion_behavior="raise",
)
References
Development
This project uses uv for package management and just as a command runner.
Prerequisites
# Install uv (if not already installed)
curl -LsSf https://astral.sh/uv/install.sh | sh
# Install just (macOS)
brew install just
Setup
git clone https://github.com/CaptainDriftwood/pytest-uuid.git
cd pytest-uuid
just sync
Available Commands
just # List all commands
just test # Run tests
just test-cov # Run tests with coverage
just nox # Run tests across all Python versions with nox
just nox 3.12 # Run tests for a specific Python version
just lint # Run linting
just format # Format code
just check # Run all checks
just build # Build the package
Coverage with Pytester
This project uses pytester for integration testing. Getting accurate coverage for pytest plugins requires special handling because plugins are imported before coverage can start measuring.
The Problem:
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:
CoverageWarning: Module pytest_uuid was previously imported, but not measured
The Solution:
Use coverage run -m pytest instead of pytest --cov:
# Instead of this:
pytest --cov=pytest_uuid --cov-report=term-missing
# Use this:
coverage run -m pytest
coverage combine
coverage report --show-missing
This works because coverage run starts measuring before Python imports anything, so the plugin import is captured.
Configuration (pyproject.toml):
[tool.coverage.run]
source = ["src/pytest_uuid"]
branch = true
parallel = true # Required for combining coverage files
patch = ["subprocess"] # Enables coverage in subprocesses
sigterm = true # Ensures coverage is saved on SIGTERM
Why parallel = true?
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.
References:
- pytest-cov Subprocess Support
- coverage.py Subprocess Measurement
- pytest-cov Issue #587 - Plugin Coverage
License
MIT License - see LICENSE for details.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pytest_uuid-1.0.0.tar.gz.
File metadata
- Download URL: pytest_uuid-1.0.0.tar.gz
- Upload date:
- Size: 51.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4bf6bcf0be4b39fdc3fc961020a408e11f62269a68746b3ef8fc04efe899cef3
|
|
| MD5 |
52b3276a2c11e7d58c9beda218011f0e
|
|
| BLAKE2b-256 |
5d2ae895a27bb07936661b60777846a2569335244459984ac7d8586fd4a866ff
|
Provenance
The following attestation bundles were made for pytest_uuid-1.0.0.tar.gz:
Publisher:
publish.yml on CaptainDriftwood/pytest-uuid
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pytest_uuid-1.0.0.tar.gz -
Subject digest:
4bf6bcf0be4b39fdc3fc961020a408e11f62269a68746b3ef8fc04efe899cef3 - Sigstore transparency entry: 1004399239
- Sigstore integration time:
-
Permalink:
CaptainDriftwood/pytest-uuid@cd84545cdd0a974640c145b99417e678adbfe0fc -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/CaptainDriftwood
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@cd84545cdd0a974640c145b99417e678adbfe0fc -
Trigger Event:
release
-
Statement type:
File details
Details for the file pytest_uuid-1.0.0-py3-none-any.whl.
File metadata
- Download URL: pytest_uuid-1.0.0-py3-none-any.whl
- Upload date:
- Size: 49.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a18098e6b7d9ad0ce84bc7f015425f48c5a5cdd4fdc54171f8de79eeb1bf0b00
|
|
| MD5 |
2c62f29aa69d5cb6b3319c240969c265
|
|
| BLAKE2b-256 |
dc1b85179c602887729b0edc1af548a7a1b28cdd1a93d5b4bac0fa0ceee69ead
|
Provenance
The following attestation bundles were made for pytest_uuid-1.0.0-py3-none-any.whl:
Publisher:
publish.yml on CaptainDriftwood/pytest-uuid
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pytest_uuid-1.0.0-py3-none-any.whl -
Subject digest:
a18098e6b7d9ad0ce84bc7f015425f48c5a5cdd4fdc54171f8de79eeb1bf0b00 - Sigstore transparency entry: 1004399246
- Sigstore integration time:
-
Permalink:
CaptainDriftwood/pytest-uuid@cd84545cdd0a974640c145b99417e678adbfe0fc -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/CaptainDriftwood
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@cd84545cdd0a974640c145b99417e678adbfe0fc -
Trigger Event:
release
-
Statement type: