Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-89083: support UUID version 7 (monotonous version) (RFC 9562) [abandoned proposal] #120830

Closed
wants to merge 15 commits into from
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`
picnixz marked this conversation as resolved.
Show resolved Hide resolved

(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
Loading