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 6 (RFC 9562) #120650

Open
wants to merge 57 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
818d417
add implementation
picnixz Jun 17, 2024
16565f2
add tests
picnixz Jun 17, 2024
e6c1d5f
add docs
picnixz Jun 17, 2024
cbaaff4
add WhatsNew
picnixz Jun 17, 2024
4ef04b9
blurb
picnixz Jun 17, 2024
943d13e
fix a mask
picnixz Jun 17, 2024
8344e64
fix random bytes generation
picnixz Jun 17, 2024
295d82d
fixup some comments
picnixz Jun 17, 2024
1aaa483
Update Lib/uuid.py
picnixz Jun 21, 2024
6847b77
Update Doc/whatsnew/3.14.rst
picnixz Jun 22, 2024
4d9862e
revert modifications on properties for now
picnixz Jun 22, 2024
08607f7
fixup
picnixz Jun 22, 2024
55edd0c
update variable names
picnixz Jun 22, 2024
5b15134
remove references to v7 and v8
picnixz Jun 28, 2024
e84cf17
Merge branch 'main' into uuid-rfc-9562
picnixz Jul 22, 2024
9bc8090
Merge branch 'main' into uuid-v6-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
a276857
add test vectors
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
4907650
Merge branch 'main' into uuid-rfc-9562
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
394a805
Merge branch 'main' into uuid-v6-89083
picnixz Nov 13, 2024
a40c19b
Merge remote-tracking branch 'origin/uuid-v8-89083' into uuid-v6-89083
picnixz Nov 13, 2024
00661fc
Merge remote-tracking branch 'origin/uuid-v8-89083'
picnixz Nov 13, 2024
c188ced
post-merge
picnixz Nov 13, 2024
7e5d364
update docs
picnixz Nov 13, 2024
b8ddc02
improve readability
picnixz Nov 13, 2024
384a02e
improve test readability
picnixz Nov 13, 2024
e4a7198
improve test coverage
picnixz Nov 13, 2024
aed5839
update docs
picnixz Nov 14, 2024
6daae22
Merge remote-tracking branch 'origin/uuid-rfc-9562' into uuid-v6-89083
picnixz Nov 14, 2024
bca3776
Merge remote-tracking branch 'upstream/main' into uuid-v6-89083
picnixz Nov 14, 2024
2df6f41
Merge remote-tracking branch 'upstream/main'
picnixz Nov 15, 2024
2d003fa
Merge branch 'main' into uuid-v6-89083
picnixz Nov 15, 2024
5ad6268
post-merge
picnixz Nov 15, 2024
d49855d
post-merge
picnixz Nov 15, 2024
a5682f8
fix comment
picnixz Nov 15, 2024
969f1c5
Merge branch 'main' into uuid-v6-89083
picnixz Nov 15, 2024
cc459dd
use versionchanged instead of versionadded
picnixz Nov 15, 2024
0d9f687
fix typo
picnixz Nov 15, 2024
d1a6cd8
Merge branch 'main' into uuid-rfc-9562
picnixz Nov 16, 2024
d70279f
Cosmetic change
picnixz Nov 16, 2024
b8a0e72
Update Doc/whatsnew/3.14.rst
picnixz Nov 16, 2024
6c6339b
fix lint
picnixz Nov 16, 2024
1e927b6
Update Lib/test/test_uuid.py
picnixz Nov 17, 2024
68394e6
Update Lib/test/test_uuid.py
picnixz Nov 17, 2024
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
28 changes: 23 additions & 5 deletions Doc/library/uuid.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@

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

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 @@ -222,6 +222,24 @@ The :mod:`uuid` module defines the following functions:
.. index:: single: uuid5


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

Generate a UUID from a sequence number and the current time according to
:rfc:`9562`.
This is an alternative to :func:`uuid1` to improve DB locality.

When *node* is not specified, :func:`getnode` is used to obtain the hardware
address as a 48-bit positive integer. When a sequence number *clock_seq* is
not specified, a pseudo-random 14-bit positive integer is generated.

If *node* or *clock_seq* exceed their expected bit count, only their least
significant bits are kept.

.. versionadded:: next

.. index:: single: uuid6


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

Generate a pseudo-random UUID according to
Expand Down Expand Up @@ -307,7 +325,7 @@ The :mod:`uuid` module can be executed as a script from the command line.

.. code-block:: sh

python -m uuid [-h] [-u {uuid1,uuid3,uuid4,uuid5,uuid8}] [-n NAMESPACE] [-N NAME]
python -m uuid [-h] [-u {uuid1,uuid3,uuid4,uuid5,uuid6,uuid8}] [-n NAMESPACE] [-N NAME]

The following options are accepted:

Expand All @@ -323,8 +341,8 @@ The following options are accepted:
Specify the function name to use to generate the uuid. By default :func:`uuid4`
is used.

.. versionadded:: next
Allow generating UUID version 8.
.. versionchanged:: next
Allow generating UUID versions 6 and 8.

.. option:: -n <namespace>
--namespace <namespace>
Expand Down
5 changes: 2 additions & 3 deletions Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -531,11 +531,10 @@ unittest
uuid
----

* Add support for UUID version 8 via :func:`uuid.uuid8` as specified
in :rfc:`9562`.
* Add support for UUID versions 6 and 8 via :func:`uuid.uuid6` and
:func:`uuid.uuid8` respectively, as specified in :rfc:`9562`.
(Contributed by Bénédikt Tran in :gh:`89083`.)


.. Add improved modules above alphabetically, not here at the end.

Optimizations
Expand Down
147 changes: 142 additions & 5 deletions Lib/test/test_uuid.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import unittest
from test import support
from test.support import import_helper
import builtins
import contextlib
import copy
Expand All @@ -10,10 +7,14 @@
import pickle
import random
import sys
import unittest
import weakref
from itertools import product
from unittest import mock

from test import support
from test.support import import_helper

py_uuid = import_helper.import_fresh_module('uuid', blocked=['_uuid'])
c_uuid = import_helper.import_fresh_module('uuid', fresh=['_uuid'])

Expand Down Expand Up @@ -590,15 +591,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 @@ -683,6 +684,142 @@ 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 = 0x1571_20a1_de1a_c533
fake_node_value = 0x54e1_acf6_da7f
fake_clock_seq = 0x14c5
with (
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)

# 32 (top) | 16 (mid) | 12 (low) == 60 (timestamp)
equal(u.time, 0x1e901fca_7a55_b92)
equal(u.fields[0], 0x1e901fca) # 32 top bits of time
equal(u.fields[1], 0x7a55) # 16 mid bits of time
# 4 bits of version + 12 low bits of time
equal((u.fields[2] >> 12) & 0xf, 6)
equal((u.fields[2] & 0xfff), 0xb92)
# 2 bits of variant + 6 high bits of clock_seq
equal((u.fields[3] >> 6) & 0xf, 2)
equal(u.fields[3] & 0x3f, fake_clock_seq >> 8)
# 8 low bits of clock_seq
equal(u.fields[4], fake_clock_seq & 0xff)
equal(u.fields[5], fake_node_value)

def test_uuid6_uniqueness(self):
# Test that UUIDv6-generated values are unique.

# Unlike UUIDv8, only 62 bits can be randomized for UUIDv6.
# In practice, however, it remains unlikely to generate two
# identical UUIDs for the same 60-bit timestamp if neither
# the node ID nor the clock sequence is specified.
uuids = {self.uuid.uuid6() for _ in range(1000)}
self.assertEqual(len(uuids), 1000)

# Within the same 60-bit timestamp, and for identical node IDs,
# it is likely to have collision on the 14-bit clock sequence.
timestamp = 0x1ec9414c_232a_b00
fake_nanoseconds = (timestamp - 0x1b21dd21_3814_000) * 100

with mock.patch('time.time_ns', return_value=fake_nanoseconds):
def gen():
with mock.patch.object(self.uuid, '_last_timestamp_v6', None):
return self.uuid.uuid6(node=0, clock_seq=None)

# By the birthday paradox, sampling N = 1024 UUIDs with the same
# node ID and timestamp results in duplicates with probabillity
# very close to 1 (the probability of not having a duplicate is
# of order 1E-15).
N = 1024
uuids = {gen() for _ in range(N)}
self.assertTrue(all(u.node == 0 for u in uuids))
self.assertTrue(all(u.time == timestamp for u in uuids))
self.assertLess(len(uuids), N, 'collision property does not hold')

def test_uuid6_node(self):
# Make sure the given node ID appears in the UUID.
#
# Note: when no node ID is specified, the same logic as for UUIDv1
# is applied to UUIDv6. In particular, there is no need to test that
# getnode() correctly returns positive integers of exactly 48 bits
# since this is done in test_uuid1_eui64().
self.assertLessEqual(self.uuid.uuid6().node.bit_length(), 48)

equal = self.assertEqual
equal(self.uuid.uuid6(0).node, 0)
equal(self.uuid.uuid6(0xffff_ffff_ffff).node, 0xffff_ffff_ffff)

for _ in range(10):
for node in (
random.getrandbits(24),
random.getrandbits(48),
random.getrandbits(72), # node with > 48 bits is truncated
):
with self.subTest(node):
u = self.uuid.uuid6(node)
equal(u.node, node & 0xffff_ffff_ffff)

def test_uuid6_clock_seq(self):
# Make sure the supplied clock sequence appears in the UUID.
#
# For UUIDv6, clock sequence bits are stored from bit 48 to bit 62,
# with the convention that the least significant bit is bit 0 and
# the most significant bit is bit 127.
get_clock_seq = lambda u: (u.int >> 48) & 0x3fff

u = self.uuid.uuid6()
self.assertLessEqual(get_clock_seq(u).bit_length(), 14)
equal = self.assertEqual

for _ in range(10):
for clock_seq in (
random.getrandbits(7),
random.getrandbits(14),
random.getrandbits(28), # clock_seq with > 14 bits is truncated
):
node = random.getrandbits(48)
with self.subTest(node=node, clock_seq=clock_seq):
u = self.uuid.uuid6(node, clock_seq)
self.assertEqual(get_clock_seq(u), clock_seq & 0x3fff)

def test_uuid6_test_vectors(self):
equal = self.assertEqual
# https://www.rfc-editor.org/rfc/rfc9562#name-test-vectors
# (separators are put at the 12th and 28th bits)
timestamp = 0x1ec9414c_232a_b00
fake_nanoseconds = (timestamp - 0x1b21dd21_3814_000) * 100
# https://www.rfc-editor.org/rfc/rfc9562#name-example-of-a-uuidv6-value
node = 0x9f6bdeced846
clock_seq = (3 << 12) | 0x3c8

with (
mock.patch.object(self.uuid, '_last_timestamp_v6', None),
mock.patch('time.time_ns', return_value=fake_nanoseconds)
):
u = self.uuid.uuid6(node=node, clock_seq=clock_seq)
equal(str(u).upper(), '1EC9414C-232A-6B00-B3C8-9F6BDECED846')
# 32 16 4 12 2 14 48
# time_hi | time_mid | ver | time_lo | var | clock_seq | node
equal(u.time, timestamp)
equal(u.int & 0xffff_ffff_ffff, node)
equal((u.int >> 48) & 0x3fff, clock_seq)
equal((u.int >> 62) & 0x3, 0b10)
equal((u.int >> 64) & 0xfff, 0xb00)
equal((u.int >> 76) & 0xf, 0x6)
equal((u.int >> 80) & 0xffff, 0x232a)
equal((u.int >> 96) & 0xffff_ffff, 0x1ec9_414c)

def test_uuid8(self):
equal = self.assertEqual
u = self.uuid.uuid8()
Expand Down
66 changes: 56 additions & 10 deletions Lib/uuid.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
r"""UUID objects (universally unique identifiers) according to RFC 4122/9562.

This module provides immutable UUID objects (class UUID) and the functions
uuid1(), uuid3(), uuid4(), uuid5(), and uuid8() for generating version 1, 3,
4, 5, and 8 UUIDs as specified in RFC 4122/9562.
uuid1(), uuid3(), uuid4(), uuid5(), uuid6(), and uuid8() for generating
version 1, 3, 4, 5, 6, and 8 UUIDs as specified in RFC 4122/9562.

If all you want is a unique ID, you should probably call uuid1() or uuid4().
Note that uuid1() may compromise privacy since it creates a UUID containing
Expand Down Expand Up @@ -108,7 +108,9 @@ class UUID:

fields a tuple of the six integer fields of the UUID,
which are also available as six individual attributes
and two derived attributes:
and two derived attributes. The time_* attributes are
only relevant to version 1, while the others are only
relevant to versions 1 and 6:

time_low the first 32 bits of the UUID
time_mid the next 16 bits of the UUID
Expand Down Expand Up @@ -322,8 +324,17 @@ def clock_seq_low(self):

@property
def time(self):
return (((self.time_hi_version & 0x0fff) << 48) |
(self.time_mid << 32) | self.time_low)
if self.version == 6:
picnixz marked this conversation as resolved.
Show resolved Hide resolved
# time_hi (32) | time_mid (16) | ver (4) | time_lo (12) | ... (64)
time_hi, time_lo = self.int >> 96, (self.int >> 64) & 0x0fff
return time_hi << 28 | (self.time_mid << 12) | time_lo
else:
# time_low (32) | time_mid (16) | ver (4) | time_hi (12) | ... (64)
#
# For compatibility purposes, we do not warn or raise when the
# version is not 1 (timestamp is irrelevant to other versions).
time_hi, time_lo = (self.int >> 64) & 0x0fff, self.int >> 96
return time_hi << 48 | (self.time_mid << 32) | time_lo

@property
def clock_seq(self):
Expand Down Expand Up @@ -656,7 +667,7 @@ def getnode():
assert False, '_random_getnode() returned invalid value: {}'.format(_node)


_last_timestamp = None
_last_timestamp_v1 = None

def uuid1(node=None, clock_seq=None):
"""Generate a UUID from a host ID, sequence number, and the current time.
Expand All @@ -674,15 +685,15 @@ def uuid1(node=None, clock_seq=None):
is_safe = SafeUUID.unknown
return UUID(bytes=uuid_time, is_safe=is_safe)

global _last_timestamp
global _last_timestamp_v1
import time
nanoseconds = time.time_ns()
# 0x01b21dd213814000 is the number of 100-ns intervals between the
# UUID epoch 1582-10-15 00:00:00 and the Unix epoch 1970-01-01 00:00:00.
timestamp = nanoseconds // 100 + 0x01b21dd213814000
if _last_timestamp is not None and timestamp <= _last_timestamp:
timestamp = _last_timestamp + 1
_last_timestamp = timestamp
if _last_timestamp_v1 is not None and timestamp <= _last_timestamp_v1:
timestamp = _last_timestamp_v1 + 1
_last_timestamp_v1 = timestamp
if clock_seq is None:
import random
clock_seq = random.getrandbits(14) # instead of stable storage
Expand Down Expand Up @@ -719,6 +730,40 @@ def uuid5(namespace, name):
hash = sha1(namespace.bytes + name).digest()
return UUID(bytes=hash[:16], version=5)

_last_timestamp_v6 = None

def uuid6(node=None, clock_seq=None):
"""Similar to :func:`uuid1` but where fields are ordered differently
for improved DB locality.

More precisely, given a 60-bit timestamp value as specified for UUIDv1,
for UUIDv6 the first 48 most significant bits are stored first, followed
by the 4-bit version (same position), followed by the remaining 12 bits
of the original 60-bit timestamp.
"""
global _last_timestamp_v6
picnixz marked this conversation as resolved.
Show resolved Hide resolved
import time
nanoseconds = time.time_ns()
# 0x01b21dd213814000 is the number of 100-ns intervals between the
# UUID epoch 1582-10-15 00:00:00 and the Unix epoch 1970-01-01 00:00:00.
timestamp = nanoseconds // 100 + 0x01b21dd213814000
if _last_timestamp_v6 is not None and timestamp <= _last_timestamp_v6:
timestamp = _last_timestamp_v6 + 1
_last_timestamp_v6 = timestamp
if clock_seq is None:
import random
clock_seq = random.getrandbits(14) # instead of stable storage
time_hi_and_mid = (timestamp >> 12) & 0xffff_ffff_ffff
time_ver_and_lo = timestamp & 0x0fff
var_and_clock_s = clock_seq & 0x3fff
if node is None:
node = getnode()
int_uuid_6 = time_hi_and_mid << 80
int_uuid_6 |= time_ver_and_lo << 64
int_uuid_6 |= var_and_clock_s << 48
int_uuid_6 |= node & 0xffff_ffff_ffff
return UUID(int=int_uuid_6, version=6)

def uuid8(a=None, b=None, c=None):
"""Generate a UUID from three custom blocks.

Expand Down Expand Up @@ -749,6 +794,7 @@ def main():
"uuid3": uuid3,
"uuid4": uuid4,
"uuid5": uuid5,
"uuid6": uuid6,
"uuid8": uuid8,
}
uuid_namespace_funcs = ("uuid3", "uuid5")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add :func:`uuid.uuid6` for generating UUIDv6 objects as specified in
:rfc:`9562`. Patch by Bénédikt Tran.
Loading