Skip to content

Commit d657777

Browse files
committed
Ensure total compatibility of C/Python implementations of uuid4 / uuid7
A side effect of some compatility fixes is the new code, with which the new C uuid7() is now 35x faster that pure Python (used to be 30x).
1 parent 68c324d commit d657777

4 files changed

Lines changed: 312 additions & 39 deletions

File tree

Lib/test/test_uuid.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1524,6 +1524,24 @@ def test_windll_getnode(self):
15241524
self.check_node(node)
15251525

15261526

1527+
class UuidHooks:
1528+
1529+
def __init__(self, *, start_at=0, inc_by=0, seed=0):
1530+
self._time = start_at
1531+
self._inc_by = inc_by
1532+
self._rnd = random.Random(seed)
1533+
1534+
def random_func (self, size) :
1535+
ret = b''
1536+
for _ in range(size) :
1537+
ret += self._rnd.getrandbits(8).to_bytes(1, 'big')
1538+
return ret
1539+
1540+
def time_func(self):
1541+
self._time += self._inc_by
1542+
return self._time
1543+
1544+
15271545
@unittest.skipUnless(c_uuid, "requires the C _uuid module")
15281546
class TestCImplementationCompat(unittest.TestCase):
15291547
def test_compatibility(self):
@@ -1595,6 +1613,55 @@ def full_test(p, u):
15951613
self.assertEqual(len(all_ps), len(all_us))
15961614
self.assertEqual(len(all_ps), len(uuids))
15971615

1616+
def _install_hooks(self, uuid_mod, *, start_at=0, inc_by=0, seed=0):
1617+
py_hooks = UuidHooks(start_at=start_at, inc_by=inc_by, seed=seed)
1618+
uuid_mod._install_py_hooks(
1619+
random_func=py_hooks.random_func,
1620+
time_func=py_hooks.time_func
1621+
)
1622+
1623+
c_hooks = UuidHooks(start_at=start_at, inc_by=inc_by, seed=seed)
1624+
uuid_mod._install_c_hooks(
1625+
random_func=c_hooks.random_func,
1626+
time_func=c_hooks.time_func
1627+
)
1628+
1629+
def _reset_hooks(self, uuid_mod):
1630+
uuid_mod._install_c_hooks(random_func=None, time_func=None)
1631+
uuid_mod._install_py_hooks(random_func=None, time_func=None)
1632+
1633+
def test_exact_same_algo_uuid4(self):
1634+
import uuid
1635+
1636+
self._install_hooks(uuid)
1637+
try:
1638+
for seq_number in range(100):
1639+
with self.subTest(seq_number=seq_number):
1640+
self.assertEqual(
1641+
uuid._py_uuid4().hex,
1642+
uuid._c_uuid4().hex,
1643+
)
1644+
finally:
1645+
self._reset_hooks(uuid)
1646+
1647+
def test_exact_same_algo_uuid7(self):
1648+
import uuid
1649+
1650+
try:
1651+
for start_at, inc_by in [
1652+
(0, 0), (0, 10_000_000), (10_000_000 + 142, 1131827398127397)
1653+
]:
1654+
self._install_hooks(uuid, start_at=start_at, inc_by=inc_by)
1655+
for seq_number in range(100):
1656+
with self.subTest(
1657+
seq_number=seq_number, start_at=start_at, inc_by=inc_by,
1658+
):
1659+
self.assertEqual(
1660+
uuid._py_uuid7().hex,
1661+
uuid._c_uuid7().hex,
1662+
)
1663+
finally:
1664+
self._reset_hooks(uuid)
15981665

15991666

16001667
if __name__ == '__main__':

Lib/uuid.py

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,33 @@ class SafeUUID:
111111
_RFC_4122_VERSION_7_FLAGS = ((7 << 76) | (0x8000 << 48))
112112
_RFC_4122_VERSION_8_FLAGS = ((8 << 76) | (0x8000 << 48))
113113

114+
_random_hook = None
115+
_time_hook = None
116+
117+
118+
def _gen_random(size):
119+
if _random_hook is None:
120+
return os.urandom(size)
121+
else:
122+
return _random_hook(size)
123+
124+
125+
def _gen_time():
126+
if _time_hook is None:
127+
return time.time_ns()
128+
else:
129+
return _time_hook()
130+
131+
132+
def _install_py_hooks(*, random_func, time_func):
133+
global _random_hook
134+
global _time_hook
135+
global _last_timestamp_v7
136+
_random_hook = random_func
137+
_time_hook = time_func
138+
# Reset _last_timestamp_v7 for repeatability of tests
139+
_last_timestamp_v7 = None
140+
114141

115142
# Import optional C extension at toplevel, to help disabling it when testing
116143
try:
@@ -783,7 +810,7 @@ def uuid3(namespace, name):
783810

784811
def uuid4():
785812
"""Generate a random UUID."""
786-
int_uuid_4 = int.from_bytes(os.urandom(16))
813+
int_uuid_4 = int.from_bytes(_gen_random(16))
787814
int_uuid_4 &= _RFC_4122_CLEARFLAGS_MASK
788815
int_uuid_4 |= _RFC_4122_VERSION_4_FLAGS
789816
return UUID._from_int(int_uuid_4)
@@ -843,7 +870,8 @@ def uuid6(node=None, clock_seq=None):
843870
_last_counter_v7 = 0 # 42-bit counter
844871

845872
def _uuid7_get_counter_and_tail():
846-
rand = int.from_bytes(os.urandom(10))
873+
rand = int.from_bytes(_gen_random(10), 'big')
874+
847875
# 42-bit counter with MSB set to 0
848876
counter = (rand >> 32) & 0x1ff_ffff_ffff
849877
# 32-bit random data
@@ -872,7 +900,7 @@ def uuid7():
872900
global _last_timestamp_v7
873901
global _last_counter_v7
874902

875-
nanoseconds = time.time_ns()
903+
nanoseconds = _gen_time()
876904
timestamp_ms = nanoseconds // 1_000_000
877905

878906
if _last_timestamp_v7 is None or timestamp_ms > _last_timestamp_v7:
@@ -888,7 +916,7 @@ def uuid7():
888916
counter, tail = _uuid7_get_counter_and_tail()
889917
else:
890918
# 32-bit random data
891-
tail = int.from_bytes(os.urandom(4))
919+
tail = int.from_bytes(_gen_random(4))
892920

893921
unix_ts_ms = timestamp_ms & 0xffff_ffff_ffff
894922
counter_msbs = counter >> 30
@@ -946,6 +974,7 @@ def uuid8(a=None, b=None, c=None):
946974
_py_UUID = UUID
947975
try:
948976
from _uuid import UUID, uuid4, uuid7
977+
from _uuid import _install_c_hooks
949978
except ImportError:
950979
_c_UUID = None
951980
_c_uuid4 = None

0 commit comments

Comments
 (0)