From 818d41748fd4cbf8f8f22ae0cb55d41bebd9f53d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Mon, 17 Jun 2024 17:29:57 +0200 Subject: [PATCH 01/15] add implementation --- Lib/uuid.py | 123 ++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 110 insertions(+), 13 deletions(-) diff --git a/Lib/uuid.py b/Lib/uuid.py index c286eac38e1ef4..fe0a6e5361035b 100644 --- a/Lib/uuid.py +++ b/Lib/uuid.py @@ -1,8 +1,9 @@ r"""UUID objects (universally unique identifiers) according to RFC 4122. This module provides immutable UUID objects (class UUID) and the functions -uuid1(), uuid3(), uuid4(), uuid5() for generating version 1, 3, 4, and 5 -UUIDs as specified in RFC 4122. +uuid1(), uuid3(), uuid4(), uuid5() for generating version 1 to 8 UUIDs as +specified in RFC 4122 (superseeded by RFC 9562 but will still be referred +to as RFC 4122 in the future for compatibility purposes). 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 @@ -129,7 +130,7 @@ class UUID: variant the UUID variant (one of the constants RESERVED_NCS, RFC_4122, RESERVED_MICROSOFT, or RESERVED_FUTURE) - version the UUID version number (1 through 5, meaningful only + version the UUID version number (1 through 8, meaningful only when the variant is RFC_4122) is_safe An enum indicating whether the UUID has been generated in @@ -214,9 +215,9 @@ def __init__(self, hex=None, bytes=None, bytes_le=None, fields=None, if not 0 <= int < 1<<128: raise ValueError('int is out of range (need a 128-bit value)') if version is not None: - if not 1 <= version <= 5: + if not 1 <= version <= 8: raise ValueError('illegal version number') - # Set the variant to RFC 4122. + # Set the variant to RFC 9562. int &= ~(0xc000 << 48) int |= 0x8000 << 48 # Set the version number. @@ -297,17 +298,29 @@ def bytes_le(self): @property def fields(self): + if self.version == 6: + # the first field should be a 32-bit integer + return (self.time_hi, self.time_mid, self.time_hi_version, + self.clock_seq_hi_variant, self.clock_seq_low, self.node) return (self.time_low, self.time_mid, self.time_hi_version, self.clock_seq_hi_variant, self.clock_seq_low, self.node) @property def time_low(self): + if self.version == 6: + return (self.int >> 64) & 0x0fff return self.int >> 96 @property def time_mid(self): return (self.int >> 80) & 0xffff + @property + def time_hi(self): + if self.version == 6: + return self.int >> 96 + return (self.int >> 64) & 0x0fff + @property def time_hi_version(self): return (self.int >> 64) & 0xffff @@ -322,8 +335,9 @@ 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: + return (self.time_hi << 28) | (self.time_mid << 12) | self.time_low + return (self.time_hi << 48) | (self.time_mid << 32) | self.time_low @property def clock_seq(self): @@ -656,7 +670,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. @@ -674,15 +688,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 @@ -719,6 +733,86 @@ 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 + 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) & 0xffffffffffff + time_ver_and_lo = timestamp & 0xffff + 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 & 0xffffffffffff + return UUID(int=int_uuid_6, version=6) + +_last_timestamp_v7 = None + +def uuid7(): + """Generate a UUID from a Unix timestamp in milliseconds and random bits.""" + global _last_timestamp_v7 + import time + nanoseconds = time.time_ns() + timestamp_ms = nanoseconds // 10 ** 6 # may be improved + if _last_timestamp_v7 is not None and timestamp_ms <= _last_timestamp_v7: + timestamp_ms = _last_timestamp_v7 + 1 + _last_timestamp_v7 = timestamp_ms + int_uuid_7 = (timestamp_ms & 0xffffffffffff) << 80 + # Ideally, we would have 'rand_a' = first 12 bits of 'rand' + # and 'rand_b' = lowest 62 bits, but it is easier to test + # when we pick 'rand_a' from the lowest bits of 'rand' and + # 'rand_b' from the next 62 bits, ignoring the first bits + # of 'rand'. + rand = int.from_bytes(os.urandom(19)) # 76 random bits (ignore 2 first) + int_uuid_7 |= (rand & 0x0fff) << 64 # rand_a + int_uuid_7 |= (rand >> 12) & 0x3fffffffffffffff # rand_b + return UUID(int=int_uuid_7, version=7) + +def uuid8(a=None, b=None, c=None): + """Generate a UUID from three custom blocks. + + 'a' is the first 48-bit chunk of the UUID (octets 0-5); + 'b' is the mid 12-bit chunk (octets 6-7); + 'c' is the last 62-bit chunk (octets 8-15). + + When a value is not specified, a random value is generated. + """ + if a is None: + import random + a = random.getrandbits(48) + if b is None: + import random + b = random.getrandbits(12) + if c is None: + import random + c = random.getrandbits(62) + + int_uuid_8 = (a & 0xffffffffffff) << 80 + int_uuid_8 |= (b & 0xfff) << 64 + int_uuid_8 |= c & 0x3fffffffffffffff + return UUID(int=int_uuid_8, version=8) + def main(): """Run the uuid command line interface.""" @@ -726,7 +820,10 @@ def main(): "uuid1": uuid1, "uuid3": uuid3, "uuid4": uuid4, - "uuid5": uuid5 + "uuid5": uuid5, + "uuid6": uuid6, + "uuid7": uuid7, + "uuid8": uuid8, } uuid_namespace_funcs = ("uuid3", "uuid5") namespaces = { From 16565f29d7197d0a2eaf25d66f4cd2ff052b6f5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Mon, 17 Jun 2024 17:30:00 +0200 Subject: [PATCH 02/15] add tests --- Lib/test/test_uuid.py | 86 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 83 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_uuid.py b/Lib/test/test_uuid.py index e177464c00f7a6..ed1c18ad8f62d6 100755 --- a/Lib/test/test_uuid.py +++ b/Lib/test/test_uuid.py @@ -1,3 +1,4 @@ +import random import unittest from test import support from test.support import import_helper @@ -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']) @@ -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)) @@ -588,7 +590,7 @@ 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 @@ -596,7 +598,7 @@ def test_uuid1_time(self): 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')) @@ -681,6 +683,84 @@ 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) + + fake_nanoseconds = 1545052026752910643 + # some fake 74 = 12 + 62 random bits speared over 76 bits + # are generated by generating a random 76-bit number, and + # split into chunks of 62 (hi) and 12 (lo) bits. It is a + rand_a = random.getrandbits(12) + rand_b = random.getrandbits(62) + fake_bytes = (rand_b << 12) | rand_a + fake_bytes = fake_bytes.to_bytes(19, byteorder='big') + + with mock.patch.object(self.uuid, '_last_timestamp_v7', None), \ + mock.patch('time.time_ns', return_value=fake_nanoseconds), \ + mock.patch('os.urandom', return_value=fake_bytes): + u = self.uuid.uuid7() + equal(u.variant, self.uuid.RFC_4122) + equal(u.version, 7) + fake_milliseconds = (fake_nanoseconds // 1_000_000) & 0xffffffffffff + equal((u.int >> 80) & 0xffffffffffff, fake_milliseconds) + equal((u.int >> 64) & 0x0fff, rand_a) + equal(u.int & 0x3fffffffffffffff, rand_b) + + 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 From e6c1d5f591127e37c4d36afcffd2b8bded9ee965 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Mon, 17 Jun 2024 17:30:02 +0200 Subject: [PATCH 03/15] add docs --- Doc/library/uuid.rst | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/Doc/library/uuid.rst b/Doc/library/uuid.rst index 0f2d7820cb25c8..0115f781b8acd6 100644 --- a/Doc/library/uuid.rst +++ b/Doc/library/uuid.rst @@ -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 @@ -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`. From cbaaff44dfb675ff766b87c7288716fece3b5533 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Mon, 17 Jun 2024 17:33:36 +0200 Subject: [PATCH 04/15] add WhatsNew --- Doc/whatsnew/3.14.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index a102af13a08362..875258c7a85121 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -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 ============= From 4ef04b907d3dcfabe2ce005ee034617abebf3b01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Mon, 17 Jun 2024 17:33:41 +0200 Subject: [PATCH 05/15] blurb --- .../next/Library/2024-06-17-17-31-27.gh-issue-89083.nW00Yq.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2024-06-17-17-31-27.gh-issue-89083.nW00Yq.rst diff --git a/Misc/NEWS.d/next/Library/2024-06-17-17-31-27.gh-issue-89083.nW00Yq.rst b/Misc/NEWS.d/next/Library/2024-06-17-17-31-27.gh-issue-89083.nW00Yq.rst new file mode 100644 index 00000000000000..55cb8bd637c2f5 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-06-17-17-31-27.gh-issue-89083.nW00Yq.rst @@ -0,0 +1,2 @@ +Add :func:`~uuid.uuid6`, :func:`~uuid.uuid7` and :func:`~uuid.uuid8` to the +:mod:`uuid` module as specified by :rfc:`9562`. Patch by Bénédikt Tran. From 943d13e5c5661ffe381586cf93301f0581c2c674 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Mon, 17 Jun 2024 17:55:00 +0200 Subject: [PATCH 06/15] fix a mask --- Lib/uuid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/uuid.py b/Lib/uuid.py index fe0a6e5361035b..d4298481c02aaf 100644 --- a/Lib/uuid.py +++ b/Lib/uuid.py @@ -757,7 +757,7 @@ def uuid6(node=None, clock_seq=None): import random clock_seq = random.getrandbits(14) # instead of stable storage time_hi_and_mid = (timestamp >> 12) & 0xffffffffffff - time_ver_and_lo = timestamp & 0xffff + time_ver_and_lo = timestamp & 0x0fff var_and_clock_s = clock_seq & 0x3fff if node is None: node = getnode() From 8344e64309ce633e42059cae3207864f387c4554 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Mon, 17 Jun 2024 18:23:01 +0200 Subject: [PATCH 07/15] fix random bytes generation --- Lib/test/test_uuid.py | 33 +++++++++++++++++---------------- Lib/uuid.py | 4 ++-- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/Lib/test/test_uuid.py b/Lib/test/test_uuid.py index ed1c18ad8f62d6..0fb167640482f7 100755 --- a/Lib/test/test_uuid.py +++ b/Lib/test/test_uuid.py @@ -721,22 +721,23 @@ def test_uuid7(self): fake_nanoseconds = 1545052026752910643 # some fake 74 = 12 + 62 random bits speared over 76 bits # are generated by generating a random 76-bit number, and - # split into chunks of 62 (hi) and 12 (lo) bits. It is a - rand_a = random.getrandbits(12) - rand_b = random.getrandbits(62) - fake_bytes = (rand_b << 12) | rand_a - fake_bytes = fake_bytes.to_bytes(19, byteorder='big') - - with mock.patch.object(self.uuid, '_last_timestamp_v7', None), \ - mock.patch('time.time_ns', return_value=fake_nanoseconds), \ - mock.patch('os.urandom', return_value=fake_bytes): - u = self.uuid.uuid7() - equal(u.variant, self.uuid.RFC_4122) - equal(u.version, 7) - fake_milliseconds = (fake_nanoseconds // 1_000_000) & 0xffffffffffff - equal((u.int >> 80) & 0xffffffffffff, fake_milliseconds) - equal((u.int >> 64) & 0x0fff, rand_a) - equal(u.int & 0x3fffffffffffffff, rand_b) + # split into chunks of 62 (hi) and 12 (lo) bits. + for _ in range(100): + rand_a = random.getrandbits(12) + rand_b = random.getrandbits(62) + fake_bytes = (rand_b << 12) | rand_a + fake_bytes = fake_bytes.to_bytes(10, byteorder='big') + + with mock.patch.object(self.uuid, '_last_timestamp_v7', None), \ + mock.patch('time.time_ns', return_value=fake_nanoseconds), \ + mock.patch('os.urandom', return_value=fake_bytes): + u = self.uuid.uuid7() + equal(u.variant, self.uuid.RFC_4122) + equal(u.version, 7) + fake_milliseconds = (fake_nanoseconds // 1_000_000) & 0xffffffffffff + equal((u.int >> 80) & 0xffffffffffff, fake_milliseconds) + equal((u.int >> 64) & 0x0fff, rand_a) + equal(u.int & 0x3fffffffffffffff, rand_b) def test_uuid8(self): equal = self.assertEqual diff --git a/Lib/uuid.py b/Lib/uuid.py index d4298481c02aaf..d9539105d9b8f7 100644 --- a/Lib/uuid.py +++ b/Lib/uuid.py @@ -782,9 +782,9 @@ def uuid7(): # Ideally, we would have 'rand_a' = first 12 bits of 'rand' # and 'rand_b' = lowest 62 bits, but it is easier to test # when we pick 'rand_a' from the lowest bits of 'rand' and - # 'rand_b' from the next 62 bits, ignoring the first bits + # 'rand_b' from the next 62 bits, ignoring the 6 first bits # of 'rand'. - rand = int.from_bytes(os.urandom(19)) # 76 random bits (ignore 2 first) + rand = int.from_bytes(os.urandom(10)) # 80 random bits (ignore 6 first) int_uuid_7 |= (rand & 0x0fff) << 64 # rand_a int_uuid_7 |= (rand >> 12) & 0x3fffffffffffffff # rand_b return UUID(int=int_uuid_7, version=7) From 295d82d513717adb96d9faab7e255dbedb237d88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Mon, 17 Jun 2024 18:38:39 +0200 Subject: [PATCH 08/15] fixup some comments --- Lib/uuid.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/uuid.py b/Lib/uuid.py index d9539105d9b8f7..0d0c226ef5e824 100644 --- a/Lib/uuid.py +++ b/Lib/uuid.py @@ -1,9 +1,9 @@ r"""UUID objects (universally unique identifiers) according to RFC 4122. This module provides immutable UUID objects (class UUID) and the functions -uuid1(), uuid3(), uuid4(), uuid5() for generating version 1 to 8 UUIDs as -specified in RFC 4122 (superseeded by RFC 9562 but will still be referred -to as RFC 4122 in the future for compatibility purposes). +uuid1(), uuid3(), uuid4(), uuid5(), uuid6(), uuid7(), and uuid8() for +generating version 1 to 8 UUIDs as specified in RFC 4122 (superseeded +by RFC 9562 but still referred to as RFC 4122 for compatibility purposes). 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 @@ -217,7 +217,7 @@ def __init__(self, hex=None, bytes=None, bytes_le=None, fields=None, if version is not None: if not 1 <= version <= 8: raise ValueError('illegal version number') - # Set the variant to RFC 9562. + # Set the variant to RFC 4122. int &= ~(0xc000 << 48) int |= 0x8000 << 48 # Set the version number. From 3345f44be4b72be8c7c605d9860a3b9217b3ae90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Fri, 21 Jun 2024 13:32:43 +0200 Subject: [PATCH 09/15] add monotonicity support for UUIDv7 --- Lib/uuid.py | 62 ++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 50 insertions(+), 12 deletions(-) diff --git a/Lib/uuid.py b/Lib/uuid.py index 0d0c226ef5e824..ea2b7a22abe385 100644 --- a/Lib/uuid.py +++ b/Lib/uuid.py @@ -768,25 +768,63 @@ def uuid6(node=None, clock_seq=None): return UUID(int=int_uuid_6, version=6) _last_timestamp_v7 = None +_last_counter_v7_a = 0 # 12-bit sub-millisecond precision +_last_counter_v7_b = 0 # 62-bit seeded counter def uuid7(): - """Generate a UUID from a Unix timestamp in milliseconds and random bits.""" + """Generate a UUID from a Unix timestamp in milliseconds and random bits. + + UUIDv7 objects feature monotonicity within a millisecond. + """ + # --- 48 --- -- 4 -- - 12 - -- 2 -- - 62 - + # unix_ts_ms | version | rand_a | variant | rand_b + # + # 'rand_a' is used for an additional 12-bit sub-millisecond + # precision constructed with Method 3 of RFC 9562, §6.2. + # + # 'rand_b' is a seeded counter generated according to + # the Method 2 of RFC 9562, §6.2. The initial counter + # is a random 62-bit integer and the counter is incremented + # by a random 32-bit integer within the same timestamp tick. + # + # If 'rand_b' overflows, it is regenerated and 'rand_a' is + # advanced by 1. If 'rand_a' also overflows, re-run uuid7(). + + def get_rand_b(): # random 62-bit integer + return int.from_bytes(os.urandom(8)) & 0x3fffffffffffffff + global _last_timestamp_v7 + global _last_counter_v7_a + global _last_counter_v7_b + import time nanoseconds = time.time_ns() - timestamp_ms = nanoseconds // 10 ** 6 # may be improved - if _last_timestamp_v7 is not None and timestamp_ms <= _last_timestamp_v7: - timestamp_ms = _last_timestamp_v7 + 1 + timestamp_ms, sub_millisecs = divmod(nanoseconds, 1_000_000) + # get the 12-bit sub-milliseconds precision part + assert 0 <= sub_millisecs < 1_000_000 + rand_a = int((sub_millisecs / 1_000_000) * (1 << 12)) + assert 0 <= rand_a <= 0xfff + + if _last_timestamp_v7 is None or timestamp_ms > _last_timestamp_v7: + rand_b = get_rand_b() + else: + if timestamp_ms < _last_timestamp_v7: + timestamp_ms = _last_timestamp_v7 + 1 + # advance 'rand_b' by a 32-bit random increment + rand_b = _last_counter_v7_b + int.from_bytes(os.urandom(4)) + if rand_b > 0x3fffffffffffffff: + if rand_a == 4095: # fast path to avoid a call to os.urandom() + return uuid7() + rand_a += 1 + rand_b = get_rand_b() + _last_timestamp_v7 = timestamp_ms + _last_counter_v7_a = rand_a + _last_counter_v7_b = rand_b + int_uuid_7 = (timestamp_ms & 0xffffffffffff) << 80 - # Ideally, we would have 'rand_a' = first 12 bits of 'rand' - # and 'rand_b' = lowest 62 bits, but it is easier to test - # when we pick 'rand_a' from the lowest bits of 'rand' and - # 'rand_b' from the next 62 bits, ignoring the 6 first bits - # of 'rand'. - rand = int.from_bytes(os.urandom(10)) # 80 random bits (ignore 6 first) - int_uuid_7 |= (rand & 0x0fff) << 64 # rand_a - int_uuid_7 |= (rand >> 12) & 0x3fffffffffffffff # rand_b + int_uuid_7 |= rand_a << 64 + int_uuid_7 |= rand_b return UUID(int=int_uuid_7, version=7) def uuid8(a=None, b=None, c=None): From 975ad209c864f95f12e5869e5df829012f0d5f72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Fri, 21 Jun 2024 13:33:17 +0200 Subject: [PATCH 10/15] add tests --- Lib/test/test_uuid.py | 165 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 145 insertions(+), 20 deletions(-) diff --git a/Lib/test/test_uuid.py b/Lib/test/test_uuid.py index 0fb167640482f7..412a294a43cc19 100755 --- a/Lib/test/test_uuid.py +++ b/Lib/test/test_uuid.py @@ -718,26 +718,151 @@ def test_uuid7(self): equal(u.variant, self.uuid.RFC_4122) equal(u.version, 7) - fake_nanoseconds = 1545052026752910643 - # some fake 74 = 12 + 62 random bits speared over 76 bits - # are generated by generating a random 76-bit number, and - # split into chunks of 62 (hi) and 12 (lo) bits. - for _ in range(100): - rand_a = random.getrandbits(12) - rand_b = random.getrandbits(62) - fake_bytes = (rand_b << 12) | rand_a - fake_bytes = fake_bytes.to_bytes(10, byteorder='big') - - with mock.patch.object(self.uuid, '_last_timestamp_v7', None), \ - mock.patch('time.time_ns', return_value=fake_nanoseconds), \ - mock.patch('os.urandom', return_value=fake_bytes): - u = self.uuid.uuid7() - equal(u.variant, self.uuid.RFC_4122) - equal(u.version, 7) - fake_milliseconds = (fake_nanoseconds // 1_000_000) & 0xffffffffffff - equal((u.int >> 80) & 0xffffffffffff, fake_milliseconds) - equal((u.int >> 64) & 0x0fff, rand_a) - equal(u.int & 0x3fffffffffffffff, rand_b) + # 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 From 6ff1aee9c04283cb0c9933554e0bdc344d61a5d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 22 Jun 2024 11:49:20 +0200 Subject: [PATCH 11/15] Update Doc/whatsnew/3.14.rst --- Doc/whatsnew/3.14.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 875258c7a85121..8e4bda42cdddc7 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -124,9 +124,9 @@ 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` + * :func:`~uuid.uuid6` + * :func:`~uuid.uuid7` + * :func:`~uuid.uuid8` (Contributed by Bénédikt Tran in :gh:`89083`.) From ed17e3616897ed0f9e81ffe4a4b1009b0f1161c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 22 Jun 2024 20:58:06 +0200 Subject: [PATCH 12/15] remove v6 and v8 to ease review --- Lib/test/test_uuid.py | 52 ------------------------------ Lib/uuid.py | 74 ------------------------------------------- 2 files changed, 126 deletions(-) diff --git a/Lib/test/test_uuid.py b/Lib/test/test_uuid.py index 412a294a43cc19..f3031b53cf0efb 100755 --- a/Lib/test/test_uuid.py +++ b/Lib/test/test_uuid.py @@ -683,35 +683,6 @@ 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() @@ -864,29 +835,6 @@ def test_uuid7_overflow_rand_a_and_rand_b(self): 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 diff --git a/Lib/uuid.py b/Lib/uuid.py index ea2b7a22abe385..0513d7457f655a 100644 --- a/Lib/uuid.py +++ b/Lib/uuid.py @@ -298,29 +298,17 @@ def bytes_le(self): @property def fields(self): - if self.version == 6: - # the first field should be a 32-bit integer - return (self.time_hi, self.time_mid, self.time_hi_version, - self.clock_seq_hi_variant, self.clock_seq_low, self.node) return (self.time_low, self.time_mid, self.time_hi_version, self.clock_seq_hi_variant, self.clock_seq_low, self.node) @property def time_low(self): - if self.version == 6: - return (self.int >> 64) & 0x0fff return self.int >> 96 @property def time_mid(self): return (self.int >> 80) & 0xffff - @property - def time_hi(self): - if self.version == 6: - return self.int >> 96 - return (self.int >> 64) & 0x0fff - @property def time_hi_version(self): return (self.int >> 64) & 0xffff @@ -335,8 +323,6 @@ def clock_seq_low(self): @property def time(self): - if self.version == 6: - return (self.time_hi << 28) | (self.time_mid << 12) | self.time_low return (self.time_hi << 48) | (self.time_mid << 32) | self.time_low @property @@ -733,40 +719,6 @@ 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 - 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) & 0xffffffffffff - 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 & 0xffffffffffff - return UUID(int=int_uuid_6, version=6) - _last_timestamp_v7 = None _last_counter_v7_a = 0 # 12-bit sub-millisecond precision _last_counter_v7_b = 0 # 62-bit seeded counter @@ -827,30 +779,6 @@ def get_rand_b(): # random 62-bit integer int_uuid_7 |= rand_b return UUID(int=int_uuid_7, version=7) -def uuid8(a=None, b=None, c=None): - """Generate a UUID from three custom blocks. - - 'a' is the first 48-bit chunk of the UUID (octets 0-5); - 'b' is the mid 12-bit chunk (octets 6-7); - 'c' is the last 62-bit chunk (octets 8-15). - - When a value is not specified, a random value is generated. - """ - if a is None: - import random - a = random.getrandbits(48) - if b is None: - import random - b = random.getrandbits(12) - if c is None: - import random - c = random.getrandbits(62) - - int_uuid_8 = (a & 0xffffffffffff) << 80 - int_uuid_8 |= (b & 0xfff) << 64 - int_uuid_8 |= c & 0x3fffffffffffffff - return UUID(int=int_uuid_8, version=8) - def main(): """Run the uuid command line interface.""" @@ -859,9 +787,7 @@ def main(): "uuid3": uuid3, "uuid4": uuid4, "uuid5": uuid5, - "uuid6": uuid6, "uuid7": uuid7, - "uuid8": uuid8, } uuid_namespace_funcs = ("uuid3", "uuid5") namespaces = { From 90c4f7c257121af9c0dfa7bb49ad785ddfa2754c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 22 Jun 2024 21:01:16 +0200 Subject: [PATCH 13/15] remove references to v6 and v8 --- Doc/whatsnew/3.14.rst | 7 +------ Lib/uuid.py | 8 ++++---- .../Library/2024-06-17-17-31-27.gh-issue-89083.nW00Yq.rst | 4 ++-- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 8e4bda42cdddc7..0ddd681ecc6166 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -121,12 +121,7 @@ symtable uuid ---- -* Add support for UUID versions 6, 7, and 8 as specified by - :rfc:`9562` to the :mod:`uuid` module: - - * :func:`~uuid.uuid6` - * :func:`~uuid.uuid7` - * :func:`~uuid.uuid8` +* Add :func:`uuid.uuid7` for UUID version 7 as specified by :rfc:`9562`. (Contributed by Bénédikt Tran in :gh:`89083`.) diff --git a/Lib/uuid.py b/Lib/uuid.py index 0513d7457f655a..416053eb3c6be6 100644 --- a/Lib/uuid.py +++ b/Lib/uuid.py @@ -1,9 +1,9 @@ r"""UUID objects (universally unique identifiers) according to RFC 4122. This module provides immutable UUID objects (class UUID) and the functions -uuid1(), uuid3(), uuid4(), uuid5(), uuid6(), uuid7(), and uuid8() for -generating version 1 to 8 UUIDs as specified in RFC 4122 (superseeded -by RFC 9562 but still referred to as RFC 4122 for compatibility purposes). +uuid1(), uuid3(), uuid4(), uuid5(), and uuid7() for generating version 1 to 7 +UUIDs as specified in RFC 4122 (superseeded by RFC 9562 but still referred to +as RFC 4122 for compatibility purposes). 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 @@ -130,7 +130,7 @@ class UUID: variant the UUID variant (one of the constants RESERVED_NCS, RFC_4122, RESERVED_MICROSOFT, or RESERVED_FUTURE) - version the UUID version number (1 through 8, meaningful only + version the UUID version number (1 through 7, meaningful only when the variant is RFC_4122) is_safe An enum indicating whether the UUID has been generated in diff --git a/Misc/NEWS.d/next/Library/2024-06-17-17-31-27.gh-issue-89083.nW00Yq.rst b/Misc/NEWS.d/next/Library/2024-06-17-17-31-27.gh-issue-89083.nW00Yq.rst index 55cb8bd637c2f5..d2507e4b24d96f 100644 --- a/Misc/NEWS.d/next/Library/2024-06-17-17-31-27.gh-issue-89083.nW00Yq.rst +++ b/Misc/NEWS.d/next/Library/2024-06-17-17-31-27.gh-issue-89083.nW00Yq.rst @@ -1,2 +1,2 @@ -Add :func:`~uuid.uuid6`, :func:`~uuid.uuid7` and :func:`~uuid.uuid8` to the -:mod:`uuid` module as specified by :rfc:`9562`. Patch by Bénédikt Tran. +Add :func:`~uuid.uuid7` to the :mod:`uuid` module as specified by :rfc:`9562`. +Patch by Bénédikt Tran. From b926eea5ff45f489426ea6099dfebe2d157d6a41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 22 Jun 2024 21:03:19 +0200 Subject: [PATCH 14/15] remove references to v6 and v8 --- Doc/library/uuid.rst | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/Doc/library/uuid.rst b/Doc/library/uuid.rst index 0115f781b8acd6..52389b207e96cc 100644 --- a/Doc/library/uuid.rst +++ b/Doc/library/uuid.rst @@ -149,11 +149,11 @@ which relays any information about the UUID's safety, using this enumeration: .. attribute:: UUID.version - The UUID version number (1 through 8, meaningful only when the variant is + The UUID version number (1 through 7, meaningful only when the variant is :const:`RFC_4122`). .. versionadded:: 3.14 - Added UUID versions 6, 7, and 8. + Added UUID version 7 .. attribute:: UUID.is_safe @@ -220,15 +220,6 @@ 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 @@ -238,15 +229,6 @@ The :mod:`uuid` module defines the following functions: .. 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`. From 5b6f34e3375282c2ad5f06eda95ff26843f0eefd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 22 Jun 2024 21:05:12 +0200 Subject: [PATCH 15/15] fixup --- Lib/uuid.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Lib/uuid.py b/Lib/uuid.py index 416053eb3c6be6..1f91043b428ac5 100644 --- a/Lib/uuid.py +++ b/Lib/uuid.py @@ -1,9 +1,9 @@ r"""UUID objects (universally unique identifiers) according to RFC 4122. This module provides immutable UUID objects (class UUID) and the functions -uuid1(), uuid3(), uuid4(), uuid5(), and uuid7() for generating version 1 to 7 -UUIDs as specified in RFC 4122 (superseeded by RFC 9562 but still referred to -as RFC 4122 for compatibility purposes). +uuid1(), uuid3(), uuid4(), uuid5(), and uuid7() for generating version 1, 3, +4, 5, and 7 UUIDs as specified in RFC 4122 (superseeded by RFC 9562 but still +referred to as RFC 4122 for compatibility purposes). 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 @@ -130,7 +130,7 @@ class UUID: variant the UUID variant (one of the constants RESERVED_NCS, RFC_4122, RESERVED_MICROSOFT, or RESERVED_FUTURE) - version the UUID version number (1 through 7, meaningful only + version the UUID version number (1, 3, 4, 5 and 7, meaningful only when the variant is RFC_4122) is_safe An enum indicating whether the UUID has been generated in @@ -215,7 +215,7 @@ def __init__(self, hex=None, bytes=None, bytes_le=None, fields=None, if not 0 <= int < 1<<128: raise ValueError('int is out of range (need a 128-bit value)') if version is not None: - if not 1 <= version <= 8: + if not 1 <= version <= 7: raise ValueError('illegal version number') # Set the variant to RFC 4122. int &= ~(0xc000 << 48) @@ -323,7 +323,8 @@ def clock_seq_low(self): @property def time(self): - return (self.time_hi << 48) | (self.time_mid << 32) | self.time_low + return (((self.time_hi_version & 0x0fff) << 48) | + (self.time_mid << 32) | self.time_low) @property def clock_seq(self):