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 8 (RFC 9562) #123224

Merged
merged 14 commits into from
Nov 12, 2024
18 changes: 16 additions & 2 deletions Doc/library/uuid.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 8 UUIDs as specified in :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 @@ -149,9 +149,13 @@ 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`).

.. versionchanged:: 3.14
picnixz marked this conversation as resolved.
Show resolved Hide resolved
Added UUID version 8.


.. attribute:: UUID.is_safe

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

.. index:: single: uuid5


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

TODO
picnixz marked this conversation as resolved.
Show resolved Hide resolved

.. versionadded:: 3.14
picnixz marked this conversation as resolved.
Show resolved Hide resolved

.. index:: single: uuid8


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

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 @@ -199,6 +199,14 @@ symtable

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

uuid
----

* Add support for UUID version 8 via :func:`uuid.uuid8` 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
27 changes: 26 additions & 1 deletion Lib/test/test_uuid.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
import io
import os
import pickle
import random
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 @@ -681,6 +683,29 @@ def test_uuid5(self):
equal(u, self.uuid.UUID(v))
equal(str(u), v)

def test_uuid8(self):
equal = self.assertEqual
u = self.uuid.uuid8()
picnixz marked this conversation as resolved.
Show resolved Hide resolved

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
33 changes: 28 additions & 5 deletions Lib/uuid.py
Original file line number Diff line number Diff line change
@@ -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(), and uuid8() for generating version 1, 3,
4, 5, and 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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -214,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 <= 5:
if not 1 <= version <= 8:
raise ValueError('illegal version number')
# Set the variant to RFC 4122.
int &= ~(0xc000 << 48)
Expand Down Expand Up @@ -719,14 +720,36 @@ def uuid5(namespace, name):
hash = sha1(namespace.bytes + name).digest()
return UUID(bytes=hash[:16], version=5)

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:
picnixz marked this conversation as resolved.
Show resolved Hide resolved
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)
Copy link
Member

@vstinner vstinner Sep 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it make sense / is it possible to reject values outside the expected value range? For example, reject negative numbers?

Maybe something like:

orig_a = a
a &= 0xffff_ffff_ffff
if a != orig_a: raise ValueError("...")

I don't know. Would it be consistent with other uuid functions?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UUIDv1 does not reject them so I wouldn't worry too much about it. v3 and v5 are not based on integral inputs.

Copy link
Member

@vstinner vstinner Sep 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with having the same behavior than uuid1() in this case, since it's documented.


int_uuid_8 = (a & 0xffffffffffff) << 80
picnixz marked this conversation as resolved.
Show resolved Hide resolved
int_uuid_8 |= (b & 0xfff) << 64
int_uuid_8 |= c & 0x3fffffffffffffff
picnixz marked this conversation as resolved.
Show resolved Hide resolved
return UUID(int=int_uuid_8, version=8)

def main():
"""Run the uuid command line interface."""
uuid_funcs = {
"uuid1": uuid1,
"uuid3": uuid3,
"uuid4": uuid4,
"uuid5": uuid5
"uuid5": uuid5,
"uuid8": uuid8,
}
uuid_namespace_funcs = ("uuid3", "uuid5")
namespaces = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add :func:`uuid.uuid8` for generating UUIDv8 objects as specified in
:rfc:`9562`. Patch by Bénédikt Tran
Loading