Skip to content
33 changes: 32 additions & 1 deletion Doc/library/uuid.rst
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,12 @@ which relays any information about the UUID's safety, using this enumeration:

.. attribute:: UUID.version

The UUID version number (1 through 5, meaningful only when the variant is
The UUID version number (1 through 8, meaningful only when the variant is
:const:`RFC_4122`).

.. versionadded:: 3.14
Added UUID versions 6, 7, and 8.

.. attribute:: UUID.is_safe

An enumeration of :class:`SafeUUID` which indicates whether the platform
Expand Down Expand Up @@ -216,6 +219,34 @@ The :mod:`uuid` module defines the following functions:

.. index:: single: uuid5


.. function:: uuid6(node=None, clock_seq=None)

TODO

.. versionadded:: 3.14

.. index:: single: uuid6


.. function:: uuid7()

TODO

.. versionadded:: 3.14

.. index:: single: uuid7


.. function:: uuid8(a=None, b=None, c=None)

TODO

.. versionadded:: 3.14

.. index:: single: uuid8


The :mod:`uuid` module defines the following namespace identifiers for use with
:func:`uuid3` or :func:`uuid5`.

Expand Down
11 changes: 11 additions & 0 deletions Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,17 @@ symtable

(Contributed by Bénédikt Tran in :gh:`120029`.)

uuid
----

* Add support for UUID versions 6, 7, and 8 as specified by
:rfc:`9562` to the :mod:`uuid` module:

* :meth:`~uuid.uuid6`
* :meth:`~uuid.uuid7`
* :meth:`~uuid.uuid8`

(Contributed by Bénédikt Tran in :gh:`89083`.)

Optimizations
=============
Expand Down
212 changes: 209 additions & 3 deletions Lib/test/test_uuid.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import random
import unittest
from test import support
from test.support import import_helper
Expand All @@ -10,6 +11,7 @@
import pickle
import sys
import weakref
from itertools import product
from unittest import mock

py_uuid = import_helper.import_fresh_module('uuid', blocked=['_uuid'])
Expand Down Expand Up @@ -267,7 +269,7 @@ def test_exceptions(self):

# Version number out of range.
badvalue(lambda: self.uuid.UUID('00'*16, version=0))
badvalue(lambda: self.uuid.UUID('00'*16, version=6))
badvalue(lambda: self.uuid.UUID('00'*16, version=42))

# Integer value out of range.
badvalue(lambda: self.uuid.UUID(int=-1))
Expand Down Expand Up @@ -588,15 +590,15 @@ def test_uuid1_bogus_return_value(self):

def test_uuid1_time(self):
with mock.patch.object(self.uuid, '_generate_time_safe', None), \
mock.patch.object(self.uuid, '_last_timestamp', None), \
mock.patch.object(self.uuid, '_last_timestamp_v1', None), \
mock.patch.object(self.uuid, 'getnode', return_value=93328246233727), \
mock.patch('time.time_ns', return_value=1545052026752910643), \
mock.patch('random.getrandbits', return_value=5317): # guaranteed to be random
u = self.uuid.uuid1()
self.assertEqual(u, self.uuid.UUID('a7a55b92-01fc-11e9-94c5-54e1acf6da7f'))

with mock.patch.object(self.uuid, '_generate_time_safe', None), \
mock.patch.object(self.uuid, '_last_timestamp', None), \
mock.patch.object(self.uuid, '_last_timestamp_v1', None), \
mock.patch('time.time_ns', return_value=1545052026752910643):
u = self.uuid.uuid1(node=93328246233727, clock_seq=5317)
self.assertEqual(u, self.uuid.UUID('a7a55b92-01fc-11e9-94c5-54e1acf6da7f'))
Expand Down Expand Up @@ -681,6 +683,210 @@ def test_uuid5(self):
equal(u, self.uuid.UUID(v))
equal(str(u), v)

def test_uuid6(self):
equal = self.assertEqual
u = self.uuid.uuid6()
equal(u.variant, self.uuid.RFC_4122)
equal(u.version, 6)

fake_nanoseconds = 1545052026752910643
fake_node_value = 93328246233727
fake_clock_seq = 5317
with mock.patch.object(self.uuid, '_generate_time_safe', None), \
mock.patch.object(self.uuid, '_last_timestamp_v6', None), \
mock.patch.object(self.uuid, 'getnode', return_value=fake_node_value), \
mock.patch('time.time_ns', return_value=fake_nanoseconds), \
mock.patch('random.getrandbits', return_value=fake_clock_seq):
u = self.uuid.uuid6()
equal(u.variant, self.uuid.RFC_4122)
equal(u.version, 6)

# time_hi time_mid time_lo
# 00011110100100000001111111001010 0111101001010101 101110010010
timestamp = 137643448267529106
equal(u.time_hi, 0b00011110100100000001111111001010)
equal(u.time_mid, 0b0111101001010101)
equal(u.time_low, 0b101110010010)
equal(u.time, timestamp)
equal(u.fields[0], u.time_hi)
equal(u.fields[1], u.time_mid)
equal(u.fields[2], u.time_hi_version)

def test_uuid7(self):
equal = self.assertEqual
u = self.uuid.uuid7()
equal(u.variant, self.uuid.RFC_4122)
equal(u.version, 7)

# 1 Jan 2023 12:34:56.123_456_789
fake_nanoseconds = 1672533296_123_456_789 # ns precision
expect_timestamp, _ = divmod(fake_nanoseconds, 1_000_000)
rand_b_64_bytes = os.urandom(8)
with mock.patch.object(self.uuid, '_last_timestamp_v7', None), \
mock.patch.object(self.uuid, '_last_counter_v7_a', 0), \
mock.patch.object(self.uuid, '_last_counter_v7_b', 0), \
mock.patch('time.time_ns', return_value=fake_nanoseconds), \
mock.patch('os.urandom', return_value=rand_b_64_bytes):
u = self.uuid.uuid7()
equal(u.variant, self.uuid.RFC_4122)
equal(u.version, 7)
equal(self.uuid._last_timestamp_v7, expect_timestamp)
unix_ts_ms = expect_timestamp & 0xffffffffffff
equal((u.int >> 80) & 0xffffffffffff, unix_ts_ms)
rand_a = 1871 # == int(0.4567890 * 4096)
equal((u.int >> 64) & 0x0fff, rand_a)
rand_b = int.from_bytes(rand_b_64_bytes) & 0x3fffffffffffffff
equal(u.int & 0x3fffffffffffffff, rand_b)

def test_uuid7_monotonicity(self):
equal = self.assertEqual

us = [self.uuid.uuid7() for _ in range(10_000)]
equal(us, sorted(us))

with mock.patch.multiple(self.uuid, _last_counter_v7_a=0, _last_counter_v7_b=0):
# 1 Jan 2023 12:34:56.123_456_789
fake_nanoseconds = 1672533296_123_456_789 # ns precision
expect_timestamp, _ = divmod(fake_nanoseconds, 1_000_000)
with mock.patch.object(self.uuid, '_last_timestamp_v7', expect_timestamp):
with mock.patch('time.time_ns', return_value=fake_nanoseconds), \
mock.patch('os.urandom', return_value=b'\x01') as os_urandom_fake:
u1 = self.uuid.uuid7()
os_urandom_fake.assert_called_once_with(4)
# 1871 = int(0.456_789 * 4096)
equal(self.uuid._last_counter_v7_a, 1871)
equal((u1.int >> 64) & 0x0fff, 1871)
equal(self.uuid._last_counter_v7_b, 1)
equal(u1.int & 0x3fffffffffffffff, 1)

# 1 Jan 2023 12:34:56.123_457_032 (same millisecond but not same prec)
next_fake_nanoseconds = 1672533296_123_457_032
with mock.patch('time.time_ns', return_value=next_fake_nanoseconds), \
mock.patch('os.urandom', return_value=b'\x01') as os_urandom_fake:
u2 = self.uuid.uuid7()
os_urandom_fake.assert_called_once_with(4)
# 1872 = int(0.457_032 * 4096)
equal(self.uuid._last_counter_v7_a, 1872)
equal((u2.int >> 64) & 0x0fff, 1872)
equal(self.uuid._last_counter_v7_b, 2)
equal(u2.int & 0x3fffffffffffffff, 2)

self.assertLess(u1, u2)
# 48-bit time component is the same
self.assertEqual(u1.int >> 80, u2.int >> 80)

def test_uuid7_timestamp_backwards(self):
equal = self.assertEqual
# 1 Jan 2023 12:34:56.123_456_789
fake_nanoseconds = 1672533296_123_456_789 # ns precision
expect_timestamp, _ = divmod(fake_nanoseconds, 1_000_000)
fake_last_timestamp_v7 = expect_timestamp + 1
fake_prev_rand_b = 123456
with mock.patch.object(self.uuid, '_last_timestamp_v7', fake_last_timestamp_v7), \
mock.patch.object(self.uuid, '_last_counter_v7_a', 0), \
mock.patch.object(self.uuid, '_last_counter_v7_b', fake_prev_rand_b), \
mock.patch('time.time_ns', return_value=fake_nanoseconds), \
mock.patch('os.urandom', return_value=b'\x00\x00\x00\x01') as os_urandom_fake:
u = self.uuid.uuid7()
os_urandom_fake.assert_called_once()
equal(u.variant, self.uuid.RFC_4122)
equal(u.version, 7)
equal(self.uuid._last_timestamp_v7, fake_last_timestamp_v7 + 1)
unix_ts_ms = (fake_last_timestamp_v7 + 1) & 0xffffffffffff
equal((u.int >> 80) & 0xffffffffffff, unix_ts_ms)
rand_a = 1871 # == int(0.456789 * 4096)
equal(self.uuid._last_counter_v7_a, rand_a)
equal((u.int >> 64) & 0x0fff, rand_a)
rand_b = fake_prev_rand_b + 1 # 1 = os.urandom(4)
equal(self.uuid._last_counter_v7_b, rand_b)
equal(u.int & 0x3fffffffffffffff, rand_b)

def test_uuid7_overflow_rand_b(self):
equal = self.assertEqual
# 1 Jan 2023 12:34:56.123_456_789
fake_nanoseconds = 1672533296_123_456_789 # ns precision
expect_timestamp, _ = divmod(fake_nanoseconds, 1_000_000)
# same timestamp, but force an overflow on rand_b (not on rand_a)
new_rand_b_64_bytes = os.urandom(8)
with mock.patch.object(self.uuid, '_last_timestamp_v7', expect_timestamp), \
mock.patch.object(self.uuid, '_last_counter_v7_a', 0), \
mock.patch.object(self.uuid, '_last_counter_v7_b', 1 << 62), \
mock.patch('time.time_ns', return_value=fake_nanoseconds), \
mock.patch('os.urandom', return_value=new_rand_b_64_bytes):
u = self.uuid.uuid7()
equal(u.variant, self.uuid.RFC_4122)
equal(u.version, 7)
equal(self.uuid._last_timestamp_v7, expect_timestamp) # same
unix_ts_ms = expect_timestamp & 0xffffffffffff
equal((u.int >> 80) & 0xffffffffffff, unix_ts_ms)
rand_a = 1871 + 1 # advance 'int(0.456789 * 4096)' by 1
equal(self.uuid._last_counter_v7_a, rand_a)
equal((u.int >> 64) & 0x0fff, rand_a)
rand_b = int.from_bytes(new_rand_b_64_bytes) & 0x3fffffffffffffff
equal(self.uuid._last_counter_v7_b, rand_b)
equal(u.int & 0x3fffffffffffffff, rand_b)

def test_uuid7_overflow_rand_a_and_rand_b(self):
equal = self.assertEqual
nanoseconds = [
1672533296_123_999_999, # to hit the overflow on rand_a
1704069296_123_456_789, # to hit 'timestamp_ms > _last_timestamp_v7'
]

# 1 Jan 2023 12:34:56.123_999_999
expect_timestamp_call_1, _ = divmod(nanoseconds[0], 1_000_000)
expect_timestamp_call_2, _ = divmod(nanoseconds[1], 1_000_000)

random_bytes = [
b'\xff' * 4, # for advancing rand_b and hitting the overflow
os.urandom(8), # for the next call to uuid7(), only called for generating rand_b
]
random_bytes_iter = iter(random_bytes)
os_urandom_fake = lambda n: next(random_bytes_iter, None)

with mock.patch.object(self.uuid, '_last_timestamp_v7', expect_timestamp_call_1), \
mock.patch.object(self.uuid, '_last_counter_v7_a', 0), \
mock.patch.object(self.uuid, '_last_counter_v7_b', 1 << 62), \
mock.patch('time.time_ns', iter(nanoseconds).__next__), \
mock.patch('os.urandom', os_urandom_fake):
u = self.uuid.uuid7()
# check that random_bytes_iter is exhausted
self.assertIsNone(os.urandom(1))
equal(u.variant, self.uuid.RFC_4122)
equal(u.version, 7)
equal(self.uuid._last_timestamp_v7, expect_timestamp_call_2)
unix_ts_ms = expect_timestamp_call_2 & 0xffffffffffff
equal((u.int >> 80) & 0xffffffffffff, unix_ts_ms)
rand_a_second_call = 1871
equal(self.uuid._last_counter_v7_a, rand_a_second_call)
equal((u.int >> 64) & 0x0fff, rand_a_second_call)
rand_b_second_call = int.from_bytes(random_bytes[1]) & 0x3fffffffffffffff
equal(self.uuid._last_counter_v7_b, rand_b_second_call)
equal(u.int & 0x3fffffffffffffff, rand_b_second_call)

def test_uuid8(self):
equal = self.assertEqual
u = self.uuid.uuid8()

equal(u.variant, self.uuid.RFC_4122)
equal(u.version, 8)

for (_, hi, mid, lo) in product(
range(10), # repeat 10 times
[None, 0, random.getrandbits(48)],
[None, 0, random.getrandbits(12)],
[None, 0, random.getrandbits(62)],
):
u = self.uuid.uuid8(hi, mid, lo)
equal(u.variant, self.uuid.RFC_4122)
equal(u.version, 8)
if hi is not None:
equal((u.int >> 80) & 0xffffffffffff, hi)
if mid is not None:
equal((u.int >> 64) & 0xfff, mid)
if lo is not None:
equal(u.int & 0x3fffffffffffffff, lo)

@support.requires_fork()
def testIssue8621(self):
# On at least some versions of OSX self.uuid.uuid4 generates
Expand Down
Loading