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: add support for UUID version 7 (RFC 9562) #121119

Open
wants to merge 51 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
42d55b4
add UUIDv7 implementation
picnixz Jun 28, 2024
6826fa1
add tests
picnixz Jun 28, 2024
edc2cab
blurb
picnixz Jun 28, 2024
c6d26b6
update CHANGELOG
picnixz Jun 28, 2024
2ddb4b8
update RFC number
picnixz Jun 28, 2024
bcd1417
add TODO in the docs
picnixz Jun 28, 2024
4630c8f
Merge branch 'main' into uuid-v7-method-1
picnixz Jul 22, 2024
cd80afb
Merge branch 'main' into uuid-v7-89083
picnixz Aug 21, 2024
c3d4745
add UUIDv8 implementation
picnixz Aug 22, 2024
392d289
add tests
picnixz Aug 22, 2024
26889ea
blurb
picnixz Aug 22, 2024
44b66e6
add What's New entry
picnixz Aug 22, 2024
7be6dc4
add docs
picnixz Aug 22, 2024
8ba3d8b
Improve hexadecimal masks reading
picnixz Sep 25, 2024
a14ae9b
add uniqueness test
picnixz Sep 25, 2024
7a169c9
Update mentions to RFC 4122 to RFC 4122/9562 when possible.
picnixz Sep 25, 2024
b082c90
Update docs
picnixz Sep 25, 2024
94c70e9
Merge branch 'main' into uuid-v8-89083
picnixz Sep 25, 2024
05b7a2b
Merge branch 'main' into uuid-v7-method-1
hugovk Nov 2, 2024
275deb7
Merge branch 'main' into uuid-v8-89083
hugovk Nov 2, 2024
5e97cc3
Apply suggestions from code review
picnixz Nov 11, 2024
051f34e
Update Lib/test/test_uuid.py
picnixz Nov 11, 2024
bdf9a77
Apply suggestions from code review
picnixz Nov 11, 2024
00661fc
Merge remote-tracking branch 'origin/uuid-v8-89083'
picnixz Nov 13, 2024
0474de4
Merge remote-tracking branch 'origin/uuid-v8-89083' into uuid-v7-89083
picnixz Nov 14, 2024
a446d53
Merge remote-tracking branch 'upstream/main' into uuid-v7-89083
picnixz Nov 14, 2024
2e39072
update CLI
picnixz Nov 14, 2024
ebc1a07
Merge branch 'main' into uuid-v7-89083
picnixz Nov 14, 2024
694e07f
post-merge
picnixz Nov 14, 2024
965dbc8
Merge remote-tracking branch 'origin/uuid-v7-method-1' into uuid-v7-8…
picnixz Nov 14, 2024
7ff4368
improve readability
picnixz Nov 14, 2024
7c3cab6
post-merge
picnixz Nov 14, 2024
e758741
uniqueness test
picnixz Nov 14, 2024
c18d0c4
improve test comments
picnixz Nov 14, 2024
2df6f41
Merge remote-tracking branch 'upstream/main'
picnixz Nov 15, 2024
6fcb6a1
fix lint
picnixz Nov 15, 2024
f6048c9
Merge branch 'main' into uuid-v7-89083
picnixz Nov 15, 2024
be3f024
post-merge
picnixz Nov 15, 2024
99c6761
Merge branch 'main' into uuid-v7-89083
picnixz Nov 15, 2024
06befca
use versionchanged instead of versionadded
picnixz Nov 15, 2024
2aacadf
Merge branch 'main' into uuid-v7-method-1
picnixz Nov 16, 2024
f7f536e
Merge branch 'main' into uuid-v7-method-1
picnixz Dec 5, 2024
aee2898
improve UUIDv7 tests readability
picnixz Dec 19, 2024
1a5ac19
improve UUIDv7 uniqueness tests
picnixz Dec 19, 2024
8764b28
Merge branch 'main' into uuid-v7-method-1
picnixz Dec 21, 2024
af0baef
Merge branch 'main' into uuid-v7-method-1
picnixz Jan 11, 2025
939b5a8
Merge branch 'main' into feat/uuid/v7-89083
picnixz Jan 20, 2025
ef85b20
use `UUID._from_int` for UUIDv7 and remove `divmod` usage
picnixz Jan 20, 2025
2d08821
Merge branch 'main' into uuid-v7-method-1
picnixz Jan 20, 2025
eaa9ad4
Merge branch 'main' into uuid-v7-method-1
picnixz Feb 17, 2025
571d2fe
backport Victor's review on UUIDv6
picnixz Feb 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 22 additions & 9 deletions Doc/library/uuid.rst
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
:mod:`!uuid` --- UUID objects according to :rfc:`4122`
:mod:`!uuid` --- UUID objects according to :rfc:`9562`
======================================================

.. module:: uuid
:synopsis: UUID objects (universally unique identifiers) according to RFC 4122
:synopsis: UUID objects (universally unique identifiers) according to RFC 9562
.. moduleauthor:: Ka-Ping Yee <[email protected]>
.. sectionauthor:: George Yoshida <[email protected]>

Expand All @@ -12,7 +12,7 @@

This module provides immutable :class:`UUID` objects (the :class:`UUID` class)
and the functions :func:`uuid1`, :func:`uuid3`, :func:`uuid4`, :func:`uuid5` for
generating version 1, 3, 4, and 5 UUIDs as specified in :rfc:`4122`.
generating version 1, 3, 4, 5, and 7 UUIDs as specified in :rfc:`9562`.

If all you want is a unique ID, you should probably call :func:`uuid1` or
:func:`uuid4`. Note that :func:`uuid1` may compromise privacy since it creates
Expand Down Expand Up @@ -65,7 +65,7 @@ which relays any information about the UUID's safety, using this enumeration:

Exactly one of *hex*, *bytes*, *bytes_le*, *fields*, or *int* must be given.
The *version* argument is optional; if given, the resulting UUID will have its
variant and version number set according to :rfc:`4122`, overriding bits in the
variant and version number set according to :rfc:`9562`, overriding bits in the
given *hex*, *bytes*, *bytes_le*, *fields*, or *int*.

Comparison of UUID objects are made by way of comparing their
Expand Down Expand Up @@ -137,7 +137,7 @@ which relays any information about the UUID's safety, using this enumeration:

.. attribute:: UUID.urn

The UUID as a URN as specified in :rfc:`4122`.
The UUID as a URN as specified in :rfc:`9562`.


.. attribute:: UUID.variant
Expand All @@ -149,7 +149,7 @@ 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 7, meaningful only when the variant is
:const:`RFC_4122`).

.. attribute:: UUID.is_safe
Expand All @@ -168,7 +168,7 @@ The :mod:`uuid` module defines the following functions:
runs, it may launch a separate program, which could be quite slow. If all
attempts to obtain the hardware address fail, we choose a random 48-bit
number with the multicast bit (least significant bit of the first octet)
set to 1 as recommended in :rfc:`4122`. "Hardware address" means the MAC
set to 1 as recommended in :rfc:`9562`. "Hardware address" means the MAC
address of a network interface. On a machine with multiple network
interfaces, universally administered MAC addresses (i.e. where the second
least significant bit of the first octet is *unset*) will be preferred over
Expand Down Expand Up @@ -216,6 +216,14 @@ The :mod:`uuid` module defines the following functions:

.. index:: single: uuid5


.. function:: uuid7()

TODO

.. index:: single: uuid7


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

Expand Down Expand Up @@ -252,7 +260,12 @@ of the :attr:`~UUID.variant` attribute:

.. data:: RFC_4122

Specifies the UUID layout given in :rfc:`4122`.
Specifies the UUID layout given in :rfc:`9562`.

.. note::

For compatibility reasons, the content of the :data:`!RFC_4122` constant
is not updated to reflect the new RFC number.


.. data:: RESERVED_MICROSOFT
Expand All @@ -267,7 +280,7 @@ of the :attr:`~UUID.variant` attribute:

.. seealso::

:rfc:`4122` - A Universally Unique IDentifier (UUID) URN Namespace
:rfc:`9562` - A Universally Unique IDentifier (UUID) URN Namespace
This specification defines a Uniform Resource Name namespace for UUIDs, the
internal format of UUIDs, and methods of generating UUIDs.

Expand Down
8 changes: 8 additions & 0 deletions Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,14 @@ pickle
* Set the default protocol version on the :mod:`pickle` module to 5.
For more details, please see :ref:`pickle protocols <pickle-protocols>`.

uuid
----

* Add support for UUID version 7 via :func:`uuid.uuid7` as specified
in :rfc:`9562`.

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


Optimizations
=============
Expand Down
177 changes: 174 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 Down Expand Up @@ -267,7 +268,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 +589,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 +682,176 @@ def test_uuid5(self):
equal(u, self.uuid.UUID(v))
equal(str(u), v)

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
timestamp_ns = 1672533296_123_456_789 # ns precision
timestamp_ms, _ = divmod(timestamp_ns, 1_000_000)

for _ in range(100):
counter_hi = random.getrandbits(11)
counter_lo = random.getrandbits(30)
counter = (counter_hi << 30) | counter_lo

tail = random.getrandbits(32)
# effective number of bits is 32 + 30 + 11 = 73
random_bits = counter << 32 | tail

# set all remaining MSB of fake random bits to 1 to ensure that
# the implementation correctly remove them
random_bits = (((1 << 7) - 1) << 73) | random_bits
random_data = random_bits.to_bytes(10)

with (
mock.patch.object(self.uuid, '_last_timestamp_v7', None),
mock.patch.object(self.uuid, '_last_counter_v7', 0),
mock.patch('time.time_ns', return_value=timestamp_ns),
mock.patch('os.urandom', return_value=random_data) as urand
):
u = self.uuid.uuid7()
urand.assert_called_once_with(10)
equal(u.variant, self.uuid.RFC_4122)
equal(u.version, 7)

equal(self.uuid._last_timestamp_v7, timestamp_ms)
equal(self.uuid._last_counter_v7, counter)

unix_ts_ms = timestamp_ms & 0xffffffffffff
equal((u.int >> 80) & 0xffffffffffff, unix_ts_ms)

equal((u.int >> 75) & 1, 0) # check that the MSB is 0
equal((u.int >> 64) & 0xfff, counter_hi)
equal((u.int >> 32) & 0x3fffffff, counter_lo)
equal(u.int & 0xffffffff, tail)

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_timestamp_v7=0, _last_counter_v7=0):
# 1 Jan 2023 12:34:56.123_456_789
timestamp_ns = 1672533296_123_456_789 # ns precision
timestamp_ms, _ = divmod(timestamp_ns, 1_000_000)

counter_hi = random.getrandbits(11)
counter_lo = random.getrandbits(29) # make sure that +1 does not overflow
counter = (counter_hi << 30) | counter_lo

tail = random.getrandbits(32)
random_bits = counter << 32 | tail
random_data = random_bits.to_bytes(10)

with (
mock.patch('time.time_ns', return_value=timestamp_ns),
mock.patch('os.urandom', return_value=random_data) as urand
):
u1 = self.uuid.uuid7()
urand.assert_called_once_with(10)
equal(self.uuid._last_timestamp_v7, timestamp_ms)
equal(self.uuid._last_counter_v7, counter)
equal((u1.int >> 64) & 0xfff, counter_hi)
equal((u1.int >> 32) & 0x3fffffff, counter_lo)
equal(u1.int & 0xffffffff, tail)

# 1 Jan 2023 12:34:56.123_457_032 (same millisecond but not same prec)
next_timestamp_ns = 1672533296_123_457_032
next_timestamp_ms, _ = divmod(timestamp_ns, 1_000_000)
equal(timestamp_ms, next_timestamp_ms)

next_tail_bytes = os.urandom(4)
next_fail = int.from_bytes(next_tail_bytes)

with (
mock.patch('time.time_ns', return_value=next_timestamp_ns),
mock.patch('os.urandom', return_value=next_tail_bytes) as urand
):
u2 = self.uuid.uuid7()
urand.assert_called_once_with(4)
# same milli-second
equal(self.uuid._last_timestamp_v7, timestamp_ms)
# counter advanced by 1
equal(self.uuid._last_counter_v7, counter + 1)
equal((u2.int >> 64) & 0xfff, counter_hi)
equal((u2.int >> 32) & 0x3fffffff, counter_lo + 1)
equal(u2.int & 0xffffffff, next_fail)

self.assertLess(u1, u2)

def test_uuid7_timestamp_backwards(self):
equal = self.assertEqual
# 1 Jan 2023 12:34:56.123_456_789
timestamp_ns = 1672533296_123_456_789 # ns precision
timestamp_ms, _ = divmod(timestamp_ns, 1_000_000)
fake_last_timestamp_v7 = timestamp_ms + 1

counter_hi = random.getrandbits(11)
counter_lo = random.getrandbits(29) # make sure that +1 does not overflow
counter = (counter_hi << 30) | counter_lo

tail_bytes = os.urandom(4)
tail = int.from_bytes(tail_bytes)

with (
mock.patch.object(self.uuid, '_last_timestamp_v7', fake_last_timestamp_v7),
mock.patch.object(self.uuid, '_last_counter_v7', counter),
mock.patch('time.time_ns', return_value=timestamp_ns),
mock.patch('os.urandom', return_value=tail_bytes) as os_urandom_fake
):
u = self.uuid.uuid7()
os_urandom_fake.assert_called_once_with(4)
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)
# counter advanced by 1
equal(self.uuid._last_counter_v7, counter + 1)
equal((u.int >> 64) & 0xfff, counter_hi)
# counter advanced by 1 (constructed so that counter_hi is unchanged)
equal((u.int >> 32) & 0x3fffffff, counter_lo + 1)
equal(u.int & 0xffffffff, tail)

def test_uuid7_overflow_counter(self):
equal = self.assertEqual
# 1 Jan 2023 12:34:56.123_456_789
timestamp_ns = 1672533296_123_456_789 # ns precision
timestamp_ms, _ = divmod(timestamp_ns, 1_000_000)

new_counter_hi = random.getrandbits(11)
new_counter_lo = random.getrandbits(30)
new_counter = (new_counter_hi << 30) | new_counter_lo

tail = random.getrandbits(32)
random_bits = new_counter << 32 | tail
random_data = random_bits.to_bytes(10)

with (
mock.patch.object(self.uuid, '_last_timestamp_v7', timestamp_ms),
# same timestamp, but force an overflow on the counter
mock.patch.object(self.uuid, '_last_counter_v7', 0x3ffffffffff),
mock.patch('time.time_ns', return_value=timestamp_ns),
mock.patch('os.urandom', return_value=random_data) as urand
):
u = self.uuid.uuid7()
urand.assert_called_with(10)
equal(u.variant, self.uuid.RFC_4122)
equal(u.version, 7)
# timestamp advanced due to overflow
equal(self.uuid._last_timestamp_v7, timestamp_ms + 1)
unix_ts_ms = (timestamp_ms + 1) & 0xffffffffffff
equal((u.int >> 80) & 0xffffffffffff, unix_ts_ms)
# counter overflow, so we picked a new one
equal(self.uuid._last_counter_v7, new_counter)
equal((u.int >> 64) & 0xfff, new_counter_hi)
equal((u.int >> 32) & 0x3fffffff, new_counter_lo)
equal(u.int & 0xffffffff, tail)

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