From 4ab0483ac169122d18002f1e53b56cbfc1c92bfe Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Wed, 17 Sep 2025 17:47:04 +0100 Subject: [PATCH 01/38] Issue #gh-139122: Reimplement base UUID type, uuid4(), and uuid7() in C The C implementation considerably boosts the performance of the key UUID operations: ------------------------------------ Operation Speedup ------------------------------------ uuid4() generation 15.01x uuid7() generation 29.64x UUID from string 6.76x UUID from bytes 5.16x str(uuid) conversion 6.66x ------------------------------------ Summary of changes: * The UUID type is reimplemented in C in its entirety. * The pure-Python is kept around and is used of the C implementation isn't available for some reason. * Both implementations are tested extensively; additional tests are added to ensure that the C implementation of the type follows the pure Python implementation fully. * The Python implementation stores UUID values as int objects. The C implementation stores them as `uint8_t[16]` array. * The C implementation has faster hash() implementation but also caches the computed hash value to speedup cases when UUIDs are used as set/dict keys. * The C implementation has a freelist to make new UUID object instantiation as fast as possible. * uuid4() and uuid7() are now implmented in C. The most performance boost (10x) comes from overfetching entropy to decrease the number of _PyOS_URandom() calls. On its own it's a safe optimization with the edge case that Unix fork needs to be explicitly handled. We do that by comparing the current PID to the PID of when the random buffer was populated. * Portions of code are coming from my implementation of faster UUID in gel-python [1]. I did use AI during the development, but basically had to rewrite the code it generated to be more idiomatic and efficient. * The benchmark can be found here [2]. * This PR makes Python UUID operations as fast as they are in NodeJS and Bun runtimes. [1] https://github.com/MagicStack/py-pgproto/blob/b8109fb311a59f30f9947567a13508da9a776564/uuid.pyx [2] https://gist.github.com/1st1/f03e816f34a61e4d46c78ff98baf4818 --- Include/internal/pycore_pylifecycle.h | 4 +- Lib/test/test_uuid.py | 120 +- Lib/uuid.py | 53 +- ...-09-18-14-13-00.gh-issue-139122.m3lp66.rst | 57 + Modules/_uuidmodule.c | 1601 ++++++++++++++++- Modules/clinic/_uuidmodule.c.h | 255 +++ 6 files changed, 2052 insertions(+), 38 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-09-18-14-13-00.gh-issue-139122.m3lp66.rst create mode 100644 Modules/clinic/_uuidmodule.c.h diff --git a/Include/internal/pycore_pylifecycle.h b/Include/internal/pycore_pylifecycle.h index bfc94e3e8529b75..c213b41496f4ade 100644 --- a/Include/internal/pycore_pylifecycle.h +++ b/Include/internal/pycore_pylifecycle.h @@ -96,8 +96,8 @@ extern const char* _Py_gitversion(void); // Export for '_asyncio' shared extension PyAPI_FUNC(int) _Py_IsInterpreterFinalizing(PyInterpreterState *interp); -/* Random */ -extern int _PyOS_URandom(void *buffer, Py_ssize_t size); +// Export for '_uuid' shared extension +PyAPI_FUNC(int) _PyOS_URandom(void *buffer, Py_ssize_t size); // Export for '_random' shared extension PyAPI_FUNC(int) _PyOS_URandomNonblock(void *buffer, Py_ssize_t size); diff --git a/Lib/test/test_uuid.py b/Lib/test/test_uuid.py index 055be2994bf435e..e27aeff3efcb132 100755 --- a/Lib/test/test_uuid.py +++ b/Lib/test/test_uuid.py @@ -35,6 +35,7 @@ def get_command_stdout(command, args): class BaseTestUUID: uuid = None + is_c_uuid = False def test_nil_uuid(self): nil_uuid = self.uuid.NIL @@ -282,14 +283,16 @@ def test_exceptions(self): badvalue(lambda: self.uuid.UUID('123456781234567812345678z2345678')) # Badly formed bytes. - badvalue(lambda: self.uuid.UUID(bytes='abc')) - badvalue(lambda: self.uuid.UUID(bytes='\0'*15)) - badvalue(lambda: self.uuid.UUID(bytes='\0'*17)) + badtype(lambda: self.uuid.UUID(bytes='abc')) + badvalue(lambda: self.uuid.UUID(bytes=b'abc')) + badvalue(lambda: self.uuid.UUID(bytes=b'\0'*15)) + badvalue(lambda: self.uuid.UUID(bytes=b'\0'*17)) # Badly formed bytes_le. - badvalue(lambda: self.uuid.UUID(bytes_le='abc')) - badvalue(lambda: self.uuid.UUID(bytes_le='\0'*15)) - badvalue(lambda: self.uuid.UUID(bytes_le='\0'*17)) + badtype(lambda: self.uuid.UUID(bytes_le='abc')) + badvalue(lambda: self.uuid.UUID(bytes_le=b'abc')) + badvalue(lambda: self.uuid.UUID(bytes_le=b'\0'*15)) + badvalue(lambda: self.uuid.UUID(bytes_le=b'\0'*17)) # Badly formed fields. badvalue(lambda: self.uuid.UUID(fields=(1,))) @@ -877,12 +880,18 @@ def test_uuid6_test_vectors(self): equal((u.int >> 80) & 0xffff, 0x232a) equal((u.int >> 96) & 0xffff_ffff, 0x1ec9_414c) - def test_uuid7(self): + def test_uuid7_functional(self): equal = self.assertEqual u = self.uuid.uuid7() equal(u.variant, self.uuid.RFC_4122) equal(u.version, 7) + def test_uuid7_mock(self): + if self.is_c_uuid: + self.skipTest("C implementation of uuid7 cannot be tested with mocks") + + 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) @@ -940,7 +949,15 @@ def test_uuid7_uniqueness(self): versions = {u.version for u in uuids} self.assertSetEqual(versions, {7}) - def test_uuid7_monotonicity(self): + def test_uuid7_monotonicity_functional(self): + equal = self.assertEqual + us = [self.uuid.uuid7() for _ in range(10_000)] + equal(us, sorted(us)) + + def test_uuid7_monotonicity_mock(self): + if self.is_c_uuid: + self.skipTest("C implementation of uuid7 cannot be tested with mocks") + equal = self.assertEqual us = [self.uuid.uuid7() for _ in range(10_000)] @@ -1003,7 +1020,10 @@ def test_uuid7_monotonicity(self): self.assertLess(u1, u2) - def test_uuid7_timestamp_backwards(self): + def test_uuid7_timestamp_backwards_mock(self): + if self.is_c_uuid: + self.skipTest("C implementation of uuid7 cannot be tested with mocks") + equal = self.assertEqual # 1 Jan 2023 12:34:56.123_456_789 timestamp_ns = 1672533296_123_456_789 # ns precision @@ -1043,7 +1063,10 @@ def test_uuid7_timestamp_backwards(self): equal((u.int >> 32) & 0x3fff_ffff, counter_lo + 1) equal(u.int & 0xffff_ffff, tail) - def test_uuid7_overflow_counter(self): + def test_uuid7_overflow_counter_mock(self): + if self.is_c_uuid: + self.skipTest("C implementation of uuid7 cannot be tested with mocks") + equal = self.assertEqual # 1 Jan 2023 12:34:56.123_456_789 timestamp_ns = 1672533296_123_456_789 # ns precision @@ -1149,6 +1172,7 @@ def test_uuid_weakref(self): class CommandLineTestCases: uuid = None # to be defined in subclasses + is_c_uuid = False def do_test_standalone_uuid(self, version): stdout = io.StringIO() @@ -1313,6 +1337,7 @@ class TestUUIDWithoutExtModule(CommandLineTestCases, BaseTestUUID, unittest.Test @unittest.skipUnless(c_uuid, 'requires the C _uuid module') class TestUUIDWithExtModule(CommandLineTestCases, BaseTestUUID, unittest.TestCase): uuid = c_uuid + is_c_uuid = True def check_has_stable_libuuid_extractable_node(self): if not self.uuid._has_stable_extractable_node: @@ -1343,6 +1368,7 @@ def test_windows_getnode_from_libuuid(self): class BaseTestInternals: _uuid = py_uuid + is_c_uuid = False def check_parse_mac(self, aix): if not aix: @@ -1536,6 +1562,7 @@ class TestInternalsWithoutExtModule(BaseTestInternals, unittest.TestCase): @unittest.skipUnless(c_uuid, 'requires the C _uuid module') class TestInternalsWithExtModule(BaseTestInternals, unittest.TestCase): uuid = c_uuid + is_c_uuid = True @unittest.skipUnless(os.name == 'posix', 'requires Posix') def test_unix_getnode(self): @@ -1553,5 +1580,78 @@ def test_windll_getnode(self): self.check_node(node) +@unittest.skipUnless(c_uuid, "requires the C _uuid module") +class TestCImplementationCompat(unittest.TestCase): + def test_compatibility(self): + import uuid + + PU = uuid._py_UUID + CU = uuid._c_UUID + N = 1000 + + uuids = [ + "00000000-0000-0000-0000-000000000000", + "ffffffff-ffff-ffff-ffff-ffffffffffff", + "c0bec4fd-e4e3-050c-a362-da3f734ffd56", # regression + *(str(uuid.uuid4()) for _ in range(N)), + *(str(uuid.uuid7()) for _ in range(N)), + *(str(uuid.uuid1()) for _ in range(N)), + *(str(uuid.UUID(bytes=os.urandom(16))) for _ in range(N)), + ] + + def full_test(p, u): + self.assertEqual(p, u) + self.assertEqual(p.hex, u.hex) + self.assertEqual(p.int, u.int) + self.assertEqual(p.variant, u.variant) + self.assertEqual(p.version, u.version) + self.assertEqual(p.is_safe, u.is_safe) + self.assertEqual(p.bytes, u.bytes) + self.assertEqual(p.bytes_le, u.bytes_le) + self.assertEqual(p.fields, u.fields) + self.assertEqual(p.time_low, u.time_low) + self.assertEqual(p.time_mid, u.time_mid) + self.assertEqual(p.time_hi_version, u.time_hi_version) + self.assertEqual(p.clock_seq_hi_variant, u.clock_seq_hi_variant) + self.assertEqual(p.clock_seq_low, u.clock_seq_low) + self.assertEqual(p.node, u.node) + + all_ps = set() + all_us = set() + for uuid_str in uuids: + with self.subTest(uuid=uuid_str): + p = PU(uuid_str) + u = CU(uuid_str) + full_test(p, u) + + u2 = CU(bytes_le=p.bytes_le) + full_test(p, u2) + + u3 = CU(fields=p.fields) + full_test(p, u3) + + u4 = CU(int=p.int) + full_test(p, u4) + + u5 = CU( + hex=p.hex, + is_safe=uuid.SafeUUID.safe, + ) + full_test( + PU( + uuid_str, + is_safe=uuid.SafeUUID.safe, + ), + u5, + ) + + all_ps.add(p) + all_us.add(u) + + self.assertEqual(len(all_ps), len(all_us)) + self.assertEqual(len(all_ps), len(uuids)) + + + if __name__ == '__main__': unittest.main() diff --git a/Lib/uuid.py b/Lib/uuid.py index 3f10fed81c1bf9c..485799dfa245e15 100644 --- a/Lib/uuid.py +++ b/Lib/uuid.py @@ -99,6 +99,7 @@ class SafeUUID: _UINT_128_MAX = (1 << 128) - 1 + # 128-bit mask to clear the variant and version bits of a UUID integral value _RFC_4122_CLEARFLAGS_MASK = ~((0xf000 << 64) | (0xc000 << 48)) # RFC 4122 variant bits and version bits to activate on a UUID integral value. @@ -111,6 +112,19 @@ class SafeUUID: _RFC_4122_VERSION_8_FLAGS = ((8 << 76) | (0x8000 << 48)) +# Import optional C extension at toplevel, to help disabling it when testing +try: + import _uuid + _generate_time_safe = getattr(_uuid, "generate_time_safe", None) + _has_stable_extractable_node = _uuid.has_stable_extractable_node + _UuidCreate = getattr(_uuid, "UuidCreate", None) +except ImportError: + _uuid = None + _generate_time_safe = None + _has_stable_extractable_node = False + _UuidCreate = None + + class UUID: """Instances of the UUID class represent UUIDs as specified in RFC 4122. UUID objects are immutable, hashable, and usable as dictionary keys. @@ -219,6 +233,10 @@ def __init__(self, hex=None, bytes=None, bytes_le=None, fields=None, raise ValueError('badly formed hexadecimal UUID string') int = int_(hex, 16) elif bytes_le is not None: + if not isinstance(bytes_le, bytes_): + raise TypeError( + f'a bytes-like object is required, not {type(bytes_le).__name__!r}' + ) if len(bytes_le) != 16: raise ValueError('bytes_le is not a 16-char string') assert isinstance(bytes_le, bytes_), repr(bytes_le) @@ -226,6 +244,10 @@ def __init__(self, hex=None, bytes=None, bytes_le=None, fields=None, bytes_le[8-1:6-1:-1] + bytes_le[8:]) int = int_.from_bytes(bytes) # big endian elif bytes is not None: + if not isinstance(bytes, bytes_): + raise TypeError( + f'a bytes-like object is required, not {type(bytes).__name__!r}' + ) if len(bytes) != 16: raise ValueError('bytes is not a 16-char string') assert isinstance(bytes, bytes_), repr(bytes) @@ -234,7 +256,7 @@ def __init__(self, hex=None, bytes=None, bytes_le=None, fields=None, if len(fields) != 6: raise ValueError('fields is not a 6-tuple') (time_low, time_mid, time_hi_version, - clock_seq_hi_variant, clock_seq_low, node) = fields + clock_seq_hi_variant, clock_seq_low, node) = fields if not 0 <= time_low < (1 << 32): raise ValueError('field 1 out of range (need a 32-bit value)') if not 0 <= time_mid < (1 << 16): @@ -249,7 +271,7 @@ def __init__(self, hex=None, bytes=None, bytes_le=None, fields=None, raise ValueError('field 6 out of range (need a 48-bit value)') clock_seq = (clock_seq_hi_variant << 8) | clock_seq_low int = ((time_low << 96) | (time_mid << 80) | - (time_hi_version << 64) | (clock_seq << 48) | node) + (time_hi_version << 64) | (clock_seq << 48) | node) if not 0 <= int <= _UINT_128_MAX: raise ValueError('int is out of range (need a 128-bit value)') if version is not None: @@ -629,19 +651,6 @@ def _netstat_getnode(): return _find_mac_under_heading('netstat', '-ian', b'Address') -# Import optional C extension at toplevel, to help disabling it when testing -try: - import _uuid - _generate_time_safe = getattr(_uuid, "generate_time_safe", None) - _has_stable_extractable_node = _uuid.has_stable_extractable_node - _UuidCreate = getattr(_uuid, "UuidCreate", None) -except ImportError: - _uuid = None - _generate_time_safe = None - _has_stable_extractable_node = False - _UuidCreate = None - - def _unix_getnode(): """Get the hardware address on Unix using the _uuid extension module.""" if _generate_time_safe and _has_stable_extractable_node: @@ -932,6 +941,20 @@ def uuid8(a=None, b=None, c=None): return UUID._from_int(int_uuid_8) +_py_uuid4 = uuid4 +_py_uuid7 = uuid7 +_py_UUID = UUID +try: + from _uuid import UUID, uuid4, uuid7 +except ImportError: + _c_UUID = None + _c_uuid4 = None + _c_uuid7 = None +else: + _c_UUID = UUID + _c_uuid4 = uuid4 + _c_uuid7 = uuid7 + def main(): """Run the uuid command line interface.""" uuid_funcs = { diff --git a/Misc/NEWS.d/next/Library/2025-09-18-14-13-00.gh-issue-139122.m3lp66.rst b/Misc/NEWS.d/next/Library/2025-09-18-14-13-00.gh-issue-139122.m3lp66.rst new file mode 100644 index 000000000000000..72eb12fecf9853e --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-09-18-14-13-00.gh-issue-139122.m3lp66.rst @@ -0,0 +1,57 @@ +Reimplement base UUID type, uuid4(), and uuid7() in C + +The C implementation considerably boosts the performance of the key UUID +operations: + +------------------------------------ +Operation Speedup +------------------------------------ +uuid4() generation 15.01x +uuid7() generation 29.64x +UUID from string 6.76x +UUID from bytes 5.16x +str(uuid) conversion 6.66x +------------------------------------ + +Summary of changes: + +* The UUID type is reimplemented in C in its entirety. + +* The pure-Python is kept around and is used of the C implementation + isn't available for some reason. + +* Both implementations are tested extensively; additional tests are + added to ensure that the C implementation of the type follows the pure + Python implementation fully. + +* The Python implementation stores UUID values as int objects. The C + implementation stores them as `uint8_t[16]` array. + +* The C implementation has faster hash() implementation but also caches + the computed hash value to speedup cases when UUIDs are used as + set/dict keys. + +* The C implementation has a freelist to make new UUID object + instantiation as fast as possible. + +* uuid4() and uuid7() are now implmented in C. The most performance + boost (10x) comes from overfetching entropy to decrease the number of + _PyOS_URandom() calls. On its own it's a safe optimization with the + edge case that Unix fork needs to be explicitly handled. We do that by + comparing the current PID to the PID of when the random buffer was + populated. + +* Portions of code are coming from my implementation of faster UUID + in gel-python [1]. I did use AI during the development, but basically + had to rewrite the code it generated to be more idiomatic and + efficient. + +* The benchmark can be found here [2]. + +* This PR makes Python UUID operations as fast as they are in NodeJS and + Bun runtimes. + +[1] +https://github.com/MagicStack/py-pgproto/blob/b8109fb311a59f30f9947567a13508da9a776564/uuid.pyx + +[2] https://gist.github.com/1st1/f03e816f34a61e4d46c78ff98baf4818 diff --git a/Modules/_uuidmodule.c b/Modules/_uuidmodule.c index 3edc29a75b32ce9..e1554e0ffec7f35 100644 --- a/Modules/_uuidmodule.c +++ b/Modules/_uuidmodule.c @@ -1,15 +1,18 @@ -/* - * Python UUID module that wraps libuuid or Windows rpcrt4.dll. - * DCE compatible Universally Unique Identifier library. - */ +// UUID accelerator base type. -// Need limited C API version 3.13 for Py_mod_gil -#include "pyconfig.h" // Py_GIL_DISABLED -#ifndef Py_GIL_DISABLED -# define Py_LIMITED_API 0x030d0000 +#ifndef Py_BUILD_CORE_BUILTIN +# define Py_BUILD_CORE_MODULE 1 #endif +#include "pyconfig.h" // Py_GIL_DISABLED #include "Python.h" +#include // for strncasecmp +#include "structmember.h" // for PyMemberDef + +#include "pycore_long.h" // _PyLong_FromByteArray, _PyLong_AsByteArray +#include "pycore_pylifecycle.h" // _PyOS_URandom() +#include "pycore_time.h" // PyTime_Time + #if defined(HAVE_UUID_H) // AIX, FreeBSD, libuuid with pkgconf #include @@ -18,6 +21,16 @@ #include #endif +#ifdef HAVE_UNISTD_H +# include // getpid() +#endif +#ifdef HAVE_PROCESS_H +# include // getpid() +#endif +#ifdef MS_WINDOWS +# include // GetCurrentProcessId() +#endif + #ifdef MS_WINDOWS #include #ifndef RPC_S_OK @@ -27,6 +40,13 @@ #ifndef MS_WINDOWS + +/*[clinic input] +module _uuid +[clinic start generated code]*/ +/*[clinic end generated code: output=da39a3ee5e6b4b0d input=7cbed123a45a3859]*/ + + static PyObject * py_uuid_generate_time_safe(PyObject *Py_UNUSED(context), PyObject *Py_UNUSED(ignored)) @@ -94,13 +114,1493 @@ py_windows_has_stable_node(void) #endif /* MS_WINDOWS */ +typedef struct uuidobject { + PyObject_HEAD + uint8_t bytes[16]; + Py_hash_t cached_hash; + PyObject *is_safe; + PyObject *weakreflist; +} uuidobject; + + +// UUID Structure per RFC 9562: +// +// A UUID is 128 bits (16 bytes) represented as: +// +// String: xx xx xx xx - xx xx - Mx xx - Nx xx - xx xx xx xx xx xx +// Byte pos: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 +// ^^^^^^^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^^^^^^^^^^^^^ +// time_low mid hi seq node +// +// Byte Layout (big-endian): +// +// Bytes 0-3: time_low (32 bits) +// Bytes 4-5: time_mid (16 bits) +// Bytes 6-7: time_hi_and_version (16 bits) +// Bytes 8-9: clock_seq_and_variant (16 bits) +// Bytes 10-15: node (48 bits) +// +// Version field is located in byte 6; most significant 4 bits: +// +// Variant field is located in byte 8; most significant variable bits: +// 0xxx: Reserved for NCS compatibility +// 10xx: RFC 4122/9562 (standard) +// 110x: Reserved for Microsoft compatibility +// 111x: Reserved for future definition + +#define RANDOM_BUF_SIZE 256 +#define MAX_FREE_LIST_SIZE 32 + +/* State of the _uuid module */ +typedef struct { + PyTypeObject *UuidType; + + PyObject *safe_uuid; + PyObject *safe_uuid_safe; + PyObject *safe_uuid_unsafe; + PyObject *safe_uuid_unknown; + + PyObject *uint128_max; + + PyObject *reserved_ncs; + PyObject *rfc_4122; + PyObject *reserved_microsoft; + PyObject *reserved_future; + + // UUID v7 state + uint64_t last_timestamp_v7; + uint64_t last_counter_v7; + + // We overfetch entropy to speed up successive uuid generations; + // this enables 10x peformance boost. + uint8_t random_buf[RANDOM_BUF_SIZE]; + uint64_t random_idx; + uint64_t random_last_pid; + + // A freelist for uuid objects -- 15-20% performance boost. + uuidobject *freelist; + uint64_t freelist_size; +} uuid_state; + +#include "clinic/_uuidmodule.c.h" + +/*[clinic input] +class uuid.UUID "uuidobject *" "&UuidType" +[clinic start generated code]*/ +/*[clinic end generated code: output=da39a3ee5e6b4b0d input=84ae6e2089cffd3f]*/ + +// Forward declarations +static int from_hex(uuidobject *self, PyObject *hex); +static int from_bytes_le(uuidobject *self, Py_buffer *bytes_le); +static int from_int(uuidobject *self, PyObject *int_value, int validate); +static int from_fields(uuidobject *self, PyObject *fields); + +static uint64_t +uuid_getpid(void) { + #if !defined(MS_WINDOWS) || defined(MS_WINDOWS_DESKTOP) || defined(MS_WINDOWS_SYSTEM) + return (uint64_t)getpid(); +#else + return (uint64_t)GetCurrentProcessId(); +#endif +} + +static inline uuid_state * +get_uuid_state(PyObject *mod) +{ + uuid_state *state = PyModule_GetState(mod); + assert(state != NULL); + return state; +} + +static inline uuid_state * +get_uuid_state_by_cls(PyTypeObject *cls) +{ + uuid_state *state = (uuid_state *)PyType_GetModuleState(cls); + assert(state != NULL); + return state; +} + +// Forward declaration +static PyObject *uuid_from_bytes_array(PyTypeObject *type, uint8_t bytes[16]); + +static int +gen_random(uuid_state *state, uint8_t *bytes, Py_ssize_t size) +{ + // Overfetching & caching entropy improves the performance 10x. + // There's a precedent with NodeJS doing exact same thing for + // improving performance of their UUID implementation. + + // IMPORTANT: callers should have a critical section or a lock + // around this function. + + uint64_t pid = uuid_getpid(); + if (pid != state->random_last_pid) { + // The main concern to take core of with caching entropy is handling + // fork -- we don't want the child process to share any entropy with + // us. Luckily getpid() is fast. + state->random_last_pid = pid; + state->random_idx = RANDOM_BUF_SIZE; + } + + if (state->random_idx + size <= RANDOM_BUF_SIZE) { + memcpy(bytes, state->random_buf + state->random_idx, size); + state->random_idx += size; + } + else { + // Pure Python implementation uses os.urandom() which + // wraps _PyOS_URandom + if (_PyOS_URandom(state->random_buf, RANDOM_BUF_SIZE) < 0) { + return -1; + } + memcpy(bytes, state->random_buf, size); + state->random_idx = size; + } + return 0; +} + +/*[clinic input] +@critical_section +_uuid.uuid4 + +Generate a random UUID (version 4). +[clinic start generated code]*/ + +static PyObject * +_uuid_uuid4_impl(PyObject *module) +/*[clinic end generated code: output=b835af30d9d6efc5 input=9cfb3a3b71c25cdf]*/ +{ + uuid_state *state = get_uuid_state(module); + uint8_t bytes[16]; + + if (gen_random(state, bytes, 16) < 0) { + return NULL; + } + + // Set version (4) and variant + bytes[6] = (bytes[6] & 0x0f) | 0x40; + bytes[8] = (bytes[8] & 0x3f) | 0x80; + + return uuid_from_bytes_array(state->UuidType, bytes); +} + +static inline int +uuid7_get_counter_and_tail(uuid_state *state, uint64_t *counter, uint32_t *tail) +{ + uint8_t rand_bytes[10]; + if (gen_random(state, rand_bytes, 10) < 0) { + return -1; + } + + uint16_t high = ((uint16_t)rand_bytes[0] << 8) | rand_bytes[1]; + uint64_t low = ((uint64_t)rand_bytes[2] << 56) | + ((uint64_t)rand_bytes[3] << 48) | + ((uint64_t)rand_bytes[4] << 40) | + ((uint64_t)rand_bytes[5] << 32) | + ((uint64_t)rand_bytes[6] << 24) | + ((uint64_t)rand_bytes[7] << 16) | + ((uint64_t)rand_bytes[8] << 8) | + ((uint64_t)rand_bytes[9]); + + *counter = (((uint64_t)(high & 0x1FF) << 32) | (low >> 32)) & 0x1FFFFFFFFFF; + *tail = (uint32_t)low; + return 0; +} + + +// There's code that modifies the module state (emulating global variables +// used in the pure Python implementation.) So we're slapping a critical +// section here to make it easier to reason about the C port of this code. +/*[clinic input] +@critical_section +_uuid.uuid7 + +Generate a UUID from a Unix timestamp in milliseconds and random bits. + +UUIDv7 objects feature monotonicity within a millisecond. +[clinic start generated code]*/ + +static PyObject * +_uuid_uuid7_impl(PyObject *module) +/*[clinic end generated code: output=f301accc11162c91 input=88514d61dc785108]*/ +{ + uuid_state *state = get_uuid_state(module); + uint8_t bytes[16]; + uint64_t timestamp_ms, counter; + uint32_t tail; + + PyTime_t pytime; + if (PyTime_Time(&pytime) < 0) { + return NULL; + } + timestamp_ms = (uint64_t)(pytime / 1000000); + + if (state->last_timestamp_v7 == 0 || timestamp_ms > state->last_timestamp_v7) { + if (uuid7_get_counter_and_tail(state, &counter, &tail) < 0) { + return NULL; + } + } else { + if (timestamp_ms < state->last_timestamp_v7) { + timestamp_ms = state->last_timestamp_v7 + 1; + } + // advance the 42-bit counter + counter = state->last_counter_v7 + 1; + if (counter > 0x3FFFFFFFFFF) { + // advance the 48-bit timestamp + timestamp_ms += 1; + if (uuid7_get_counter_and_tail(state, &counter, &tail) < 0) { + return NULL; + } + } else { + // This is the common fast path, we only need 4 bytes of entropy + // 32-bit random data + if (gen_random(state, (uint8_t *)&tail, 4) < 0) { + return NULL; + } + } + } + + timestamp_ms &= 0xFFFFFFFFFFFF; + bytes[0] = (timestamp_ms >> 40); + bytes[1] = (timestamp_ms >> 32); + bytes[2] = (timestamp_ms >> 24); + bytes[3] = (timestamp_ms >> 16); + bytes[4] = (timestamp_ms >> 8); + bytes[5] = timestamp_ms; + + uint16_t counter_hi = (counter >> 30) & 0x0FFF; + bytes[6] = 0x70 | ((counter_hi >> 8)); // Version 7 = 0111 + bytes[7] = counter_hi; + + uint16_t counter_mid = (counter >> 16) & 0x3FFF; + bytes[8] = 0x80 | (counter_mid >> 8); // Variant = 10 + bytes[9] = counter_mid; + + uint16_t counter_lo = counter & 0xFFFF; + bytes[10] = counter_lo >> 8; + bytes[11] = counter_lo; + + bytes[12] = tail >> 24; + bytes[13] = tail >> 16; + bytes[14] = tail >> 8; + bytes[15] = tail; + + state->last_timestamp_v7 = timestamp_ms; + state->last_counter_v7 = counter; + + return uuid_from_bytes_array(state->UuidType, bytes); +} + +/*[clinic input] +uuid.UUID.__init__ + + hex: 'U' = NULL + bytes: 'y*' = None + bytes_le: 'y*' = None + fields: object = NULL + int: object = NULL + version: object = NULL + * + is_safe: object = NULL + +UUID is a fast base implementation type for uuid.UUID. +[clinic start generated code]*/ + +static int +_uuid_UUID___init___impl(uuidobject *self, PyObject *hex, Py_buffer *bytes, + Py_buffer *bytes_le, PyObject *fields, + PyObject *int_value, PyObject *version, + PyObject *is_safe) +/*[clinic end generated code: output=93a6881c8f79bf9b input=b9c79672fbd76a99]*/ + +{ + uuid_state *state = get_uuid_state_by_cls(Py_TYPE(self)); + + int passed = 0; + if (hex != NULL) passed++; + if (bytes->obj != NULL) passed++; + if (bytes_le->obj != NULL) passed++; + if (fields != NULL) passed++; + if (int_value != NULL) passed++; + if (passed != 1) { + PyErr_SetString( + PyExc_TypeError, + "one of the hex, bytes, bytes_le, fields, or int arguments must be given" + ); + return -1; + } + + if (hex != NULL) { + if (from_hex(self, hex) < 0) { + return -1; + } + } + else if (bytes->obj != NULL) { + if (bytes->len != 16) { + PyErr_SetString( + PyExc_ValueError, + "bytes is not a 16-char string" + ); + return -1; + } + memcpy(self->bytes, bytes->buf, 16); + } + else if (bytes_le->obj != NULL) { + if (from_bytes_le(self, bytes_le) < 0) { + return -1; + } + } + else if (fields != NULL) { + if (from_fields(self, fields) < 0) { + return -1; + } + } + else if (int_value != NULL) { + if (from_int(self, int_value, 1) < 0) { + return -1; + } + } + else { + Py_UNREACHABLE(); + } + + if (version != NULL && version != Py_None) { + long version_num = PyLong_AsLong(version); + if (version_num == -1 && PyErr_Occurred()) { + return -1; + } + if (version_num < 1 || version_num > 8) { + PyErr_SetString(PyExc_ValueError, "illegal version number"); + return -1; + } + + // Clear variant bits (keep only lower 6 bits of byte 8) + self->bytes[8] &= 0x3f; // 0011 1111 + // Clear version bits (keep only lower 4 bits of byte 6) + self->bytes[6] &= 0x0f; // 0000 1111 + // Set the variant to RFC 4122/9562 (binary 10xx xxxx) + self->bytes[8] |= 0x80; // 1000 0000 + // Set the version number (upper 4 bits of byte 6) + self->bytes[6] |= (version_num << 4); + } + + if (is_safe != NULL) { + // Validate by calling SafeUUID(is_safe) to ensure it's a valid enum member + PyObject *validated = PyObject_CallOneArg(state->safe_uuid, is_safe); + if (validated == NULL) { + return -1; + } + Py_CLEAR(self->is_safe); + self->is_safe = validated; // reuse reference + } + + return 0; +} + + +static const uint8_t INT_TO_HEX[] = "0123456789abcdef"; + +// Lookup table for hex character to value conversion +// -1 for invalid characters +static const int8_t HEX_TO_INT[256] = { + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1, 0,1,2,3,4,5,6,7,8,9,-1,-1,-1,-1,-1,-1,-1,10,11,12,13,14,15,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,10,11,12,13,14,15,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 +}; + +static inline void +byte_to_hex(uint8_t byte, char *hex) +{ + hex[0] = INT_TO_HEX[(byte >> 4) & 0xf]; + hex[1] = INT_TO_HEX[byte & 0xf]; +} + +static int +from_hex(uuidobject *self, PyObject *hex) +{ + Py_ssize_t size; + const char *start = PyUnicode_AsUTF8AndSize(hex, &size); + if (start == NULL) { + return -1; + } + + uint8_t ch; + uint8_t acc, acc_set; + int8_t part; + int i, j; + + // Reimplement `hex = hex.replace('urn:', '').replace('uuid:', '')` + if (size > 0 && start[0] == 'u') { + if (size >= 9 && strncmp(start, "urn:uuid:", 9) == 0) { + start += 9; + size -= 9; + } + else if (size >= 4 && strncmp(start, "urn:", 4) == 0) { + start += 4; + size -= 4; + } + else if (size >= 5 && strncmp(start, "uuid:", 5) == 0) { + start += 5; + size -= 5; + } + } + + // Reimplement `hex = hex.strip('{}')` + if (size >= 1 && start[0] == '{') { + start++; + size -= 1; + } + if (size >= 1 && start[size - 1] == '}') { + size -= 1; + } + + if (size < 32) { + PyErr_SetString( + PyExc_ValueError, + "badly formed hexadecimal UUID string" + ); + return -1; + } + + acc_set = 0; + j = 0; + + for (i = 0; i < size; i++) { + ch = (uint8_t)start[i]; + + if (ch == '-') { + continue; + } + + part = HEX_TO_INT[ch]; + if (part == -1) { + PyErr_SetString( + PyExc_ValueError, + "badly formed hexadecimal UUID string" + ); + return -1; + } + + if (acc_set) { + acc |= (uint8_t)part; + self->bytes[j] = acc; + acc_set = 0; + j++; + } + else { + acc = (uint8_t)part << 4; + acc_set = 1; + } + + if (j > 16 || (j == 16 && acc_set)) { + PyErr_Format(PyExc_ValueError, + "invalid UUID '%s': decodes to more than 16 bytes", + hex); + return -1; + } + } + + if (j != 16) { + PyErr_Format(PyExc_ValueError, + "invalid UUID '%s': decodes to less than 16 bytes", + hex); + return -1; + } + + return 0; +} + +static int +from_bytes_le(uuidobject *self, Py_buffer *bytes_le) +{ + if (bytes_le->len != 16) { + PyErr_SetString(PyExc_ValueError, + "bytes_le is not a 16-char string"); + return -1; + } + + // Convert from little-endian to big-endian UUID format + // UUID fields in little-endian order need to be byte-swapped: + // - time_low (4 bytes) + // - time_mid (2 bytes) + // - time_hi_version (2 bytes) + // - clock_seq_hi_variant (1 byte) - no swap needed + // - clock_seq_low (1 byte) - no swap needed + // - node (6 bytes) - no swap needed + + unsigned char *src = (unsigned char *)bytes_le->buf; + unsigned char *dst = (unsigned char *)self->bytes; + + // Swap time_low (bytes 0-3) + dst[0] = src[3]; + dst[1] = src[2]; + dst[2] = src[1]; + dst[3] = src[0]; + + // Swap time_mid (bytes 4-5) + dst[4] = src[5]; + dst[5] = src[4]; + + // Swap time_hi_version (bytes 6-7) + dst[6] = src[7]; + dst[7] = src[6]; + + // Copy clock_seq and node as-is (bytes 8-15) + memcpy(dst + 8, src + 8, 8); + + return 0; +} + +static int +validate_int(uuid_state *state, PyObject *int_value) +{ + if (!PyLong_Check(int_value)) { + PyErr_SetString(PyExc_TypeError, "value must be an integer"); + return -1; + } + + int cmp = PyLong_IsNegative(int_value); + if (cmp < 0) { + return -1; + } + if (cmp == 1) { + PyErr_SetString(PyExc_ValueError, + "int is out of range (need a 128-bit value)"); + return -1; + } + + // Check if it's greater than max (2^128 - 1) + cmp = PyObject_RichCompareBool(int_value, state->uint128_max, Py_GT); + if (cmp < 0) { + return -1; + } + if (cmp == 1) { + PyErr_SetString(PyExc_ValueError, + "int is out of range (need a 128-bit value)"); + return -1; + } + + return 0; +} + +static int +from_int(uuidobject *self, PyObject *int_value, int validate) +{ + // Convert a 128-bit integer to UUID bytes (big-endian) + + uuid_state *state = get_uuid_state_by_cls(Py_TYPE(self)); + + if (validate && validate_int(state, int_value) < 0) { + return -1; + } + + if (_PyLong_AsByteArray( + (PyLongObject *)int_value, + (unsigned char *)self->bytes, + 16, + 0, // big-endian + 0, // unsigned + 1 // with_exceptions + ) < 0) + { + return -1; + } + + return 0; +} + +static int +extract_field( + PyObject *fields, + int field_num, + uint64_t max_value, + const char *error_msg, + uint64_t *result +) { + PyObject *field = PySequence_GetItem(fields, field_num); + if (field == NULL) { + return -1; + } + + if (!PyLong_Check(field)) { + PyErr_Format(PyExc_TypeError, "field %d must be an integer", field_num); + goto fail; + } + + int overflow; + uint64_t value = PyLong_AsLongLongAndOverflow(field, &overflow); + if (overflow || (value == (uint64_t)-1 && PyErr_Occurred())) { + PyErr_Format(PyExc_ValueError, "%s", error_msg); + goto fail; + } + + if (value > max_value) { + PyErr_Format(PyExc_ValueError, "%s", error_msg); + goto fail; + } + + *result = value; + Py_DECREF(field); + return 0; + +fail: + Py_DECREF(field); + return -1; +} + +static int +from_fields(uuidobject *self, PyObject *fields) +{ + // Validate that fields is a sequence with exactly 6 elements + if (!PySequence_Check(fields)) { + PyErr_SetString(PyExc_TypeError, "fields must be a sequence"); + return -1; + } + + Py_ssize_t len = PySequence_Size(fields); + if (len != 6) { + PyErr_SetString(PyExc_ValueError, "fields is not a 6-tuple"); + return -1; + } + + #define EXTRACT_FIELD(field_num, max_value, error_msg, type, name) \ + type name; \ + uint64_t name##_extracted; \ + if (extract_field(fields, field_num, max_value, error_msg, \ + &(name##_extracted)) < 0) { \ + return -1; \ + } \ + name = (type)name##_extracted; + + EXTRACT_FIELD( + 0, (1ULL << 32) - 1, "field 1 out of range (need a 32-bit value)", + uint32_t, time_low + ); + EXTRACT_FIELD( + 1, (1ULL << 16) - 1, "field 2 out of range (need a 16-bit value)", + uint16_t, time_mid + ); + EXTRACT_FIELD( + 2, (1ULL << 16) - 1, "field 3 out of range (need a 16-bit value)", + uint16_t, time_hi_version + ); + EXTRACT_FIELD( + 3, (1ULL << 8) - 1, "field 4 out of range (need an 8-bit value)", + uint8_t, clock_seq_hi_variant + ); + EXTRACT_FIELD( + 4, (1ULL << 8) - 1, "field 5 out of range (need an 8-bit value)", + uint8_t, clock_seq_low + ); + EXTRACT_FIELD( + 5, (1ULL << 48) - 1, "field 6 out of range (need a 48-bit value)", + uint64_t, node + ); + + self->bytes[0] = time_low >> 24; + self->bytes[1] = time_low >> 16; + self->bytes[2] = time_low >> 8; + self->bytes[3] = time_low; + + self->bytes[4] = time_mid >> 8; + self->bytes[5] = time_mid; + + self->bytes[6] = time_hi_version >> 8; + self->bytes[7] = time_hi_version; + + self->bytes[8] = clock_seq_hi_variant; + + self->bytes[9] = clock_seq_low; + + self->bytes[10] = node >> 40; + self->bytes[11] = node >> 32; + self->bytes[12] = node >> 24; + self->bytes[13] = node >> 16; + self->bytes[14] = node >> 8; + self->bytes[15] = node; + + return 0; +} + +static PyObject * +get_int(uuidobject *self) +{ + return _PyLong_FromByteArray((unsigned char *)self->bytes, 16, 0, 0); +} + +static uuidobject * +make_uuid(PyTypeObject *type) +{ + uuidobject *self = NULL; + uuid_state *state = get_uuid_state_by_cls(type); + + Py_BEGIN_CRITICAL_SECTION(type); + if (state->freelist_size > 0) { + self = state->freelist; + state->freelist = (uuidobject *)self->weakreflist; + state->freelist_size--; + } + Py_END_CRITICAL_SECTION(); + + if (self != NULL) { + // Reinitialize the object from freelist + _Py_NewReference((PyObject *)self); + } + else { + self = PyObject_New(uuidobject, type); + if (self == NULL) { + return NULL; + } + } + + // During module initialization, safe_uuid_unknown might not be set yet + if (state->safe_uuid_unknown != NULL) { + self->is_safe = Py_NewRef(state->safe_uuid_unknown); + } else { + self->is_safe = Py_NewRef(Py_None); + } + + self->weakreflist = NULL; + self->cached_hash = -1; + + return self; +} + +static PyObject * +Uuid_alloc(PyTypeObject *type, PyObject *args, PyObject *kwargs) +{ + uuidobject *self = make_uuid(type); + if (self == NULL) { + return NULL; + } + memset(self->bytes, 0, 16); + return (PyObject *)self; +} + +static void +Uuid_dealloc(PyObject *obj) +{ + PyTypeObject *type = Py_TYPE(obj); + uuid_state *state = get_uuid_state_by_cls(type); + + uuidobject *uuid = (uuidobject *)obj; + if (uuid->weakreflist != NULL) { + PyObject_ClearWeakRefs(obj); + } + Py_CLEAR(uuid->is_safe); + + int added_to_freelist = 0; + Py_BEGIN_CRITICAL_SECTION(type); + if (state->freelist_size < MAX_FREE_LIST_SIZE) { + uuidobject *head = state->freelist; + state->freelist = uuid; + uuid->weakreflist = (PyObject *)head; + state->freelist_size++; + added_to_freelist = 1; + } + Py_END_CRITICAL_SECTION(); + + if (!added_to_freelist) { + type->tp_free(uuid); + // UUID is a heap allocated type so we have to decref the type ref + Py_DECREF(type); + } +} + + +static PyObject * +Uuid_get_int(uuidobject *self, void *closure) +{ + return get_int(self); +} + +static PyObject * +Uuid_get_is_safe(uuidobject *self, void *closure) +{ + if (self->is_safe == NULL) { + Py_RETURN_NONE; + } + return Py_NewRef(self->is_safe); +} + +static PyObject * +Uuid_get_hex(uuidobject *self, void *closure) +{ + char hex[32]; + for (int i = 0; i < 16; i++) { + byte_to_hex(self->bytes[i], &hex[i * 2]); + } + return PyUnicode_FromStringAndSize(hex, 32); +} + +static PyObject * +Uuid_get_variant(uuidobject *self, void *closure) +{ + uuid_state *state = get_uuid_state_by_cls(Py_TYPE(self)); + + uint8_t variant_byte = self->bytes[8]; + + // xxx - three high bits of variant_byte are unknown + if (!(variant_byte & 0x80)) { // & 0b1000_0000 + // 0xx - RESERVED_NCS + return Py_NewRef(state->reserved_ncs); + } + + // 1xx -- we know that high bit must be 1 + if (!(variant_byte & 0x40)) { // & 0b0100_0000 + // 10x - RFC_4122 + return Py_NewRef(state->rfc_4122); + } + + // 11x -- we know that two high bits are 1 + if (!(variant_byte & 0x20)) { // & 0b0010_0000 + // 110 - RESERVED_MICROSOFT + return Py_NewRef(state->reserved_microsoft); + } + + // 111 -- we know that all three high bits are 1 - RESERVED_FUTURE + return Py_NewRef(state->reserved_future); +} + +static int +is_rfc_4122(uuidobject *self) +{ + return (self->bytes[8] & 0xc0) == 0x80; +} + +static long +get_version(uuidobject *self) +{ + // RFC_4122 is when bit 7 is set (0x80) and bit 6 is not set (0x40) + // 0xc0 = 0b11000000 + // 0x80 = 0b10000000 + if (!is_rfc_4122(self)) { + return 0; + } + return (self->bytes[6] >> 4) & 0xf; +} + +static PyObject * +Uuid_get_version(uuidobject *self, void *closure) +{ + if (!is_rfc_4122(self)) { + Py_RETURN_NONE; + } + return PyLong_FromLong(get_version(self)); +} + +static inline uint32_t +get_time_low(uuidobject *self) +{ + return ((uint32_t)self->bytes[0] << 24) | + ((uint32_t)self->bytes[1] << 16) | + ((uint32_t)self->bytes[2] << 8) | + ((uint32_t)self->bytes[3]); +} + +static inline uint16_t +get_time_mid(uuidobject *self) +{ + return ((uint16_t)self->bytes[4] << 8) | + ((uint16_t)self->bytes[5]); +} + +static inline uint16_t +get_time_hi_version(uuidobject *self) +{ + return ((uint16_t)self->bytes[6] << 8) | + ((uint16_t)self->bytes[7]); +} + +static inline uint8_t +get_clock_seq_hi_variant(uuidobject *self) +{ + return self->bytes[8]; +} + +static inline uint8_t +get_clock_seq_low(uuidobject *self) +{ + return self->bytes[9]; +} + +static inline uint64_t +get_node(uuidobject *self) +{ + return ((uint64_t)self->bytes[10] << 40) | + ((uint64_t)self->bytes[11] << 32) | + ((uint64_t)self->bytes[12] << 24) | + ((uint64_t)self->bytes[13] << 16) | + ((uint64_t)self->bytes[14] << 8) | + ((uint64_t)self->bytes[15]); +} + +static PyObject * +Uuid_get_time_low(uuidobject *self, void *closure) +{ + return PyLong_FromUnsignedLong(get_time_low(self)); +} + +static PyObject * +Uuid_get_time_mid(uuidobject *self, void *closure) +{ + return PyLong_FromUnsignedLong(get_time_mid(self)); +} + +static PyObject * +Uuid_get_time_hi_version(uuidobject *self, void *closure) +{ + return PyLong_FromUnsignedLong(get_time_hi_version(self)); +} + +static PyObject * +Uuid_get_clock_seq_hi_variant(uuidobject *self, void *closure) +{ + return PyLong_FromUnsignedLong(get_clock_seq_hi_variant(self)); +} + +static PyObject * +Uuid_get_clock_seq_low(uuidobject *self, void *closure) +{ + return PyLong_FromUnsignedLong(get_clock_seq_low(self)); +} + +static PyObject * +Uuid_get_time(uuidobject *self, void *closure) +{ + long version = get_version(self); + + if (version == 6) { + // UUID v6: time_hi (32) | time_mid (16) | ver (4) | time_lo (12) | ... (64) + uint32_t time_hi = get_time_low(self); + uint16_t time_mid = get_time_mid(self); + uint16_t time_lo = ((uint16_t)(self->bytes[6] & 0x0f) << 8) | + ((uint16_t)self->bytes[7]); + + uint64_t time = ((uint64_t)time_hi << 28) | + ((uint64_t)time_mid << 12) | + (uint64_t)time_lo; + return PyLong_FromUnsignedLongLong(time); + } + else if (version == 7) { + // UUID v7: unix_ts_ms (48) | ... (80) + // First 6 bytes are the 48-bit timestamp + uint64_t unix_ts_ms = ((uint64_t)self->bytes[0] << 40) | + ((uint64_t)self->bytes[1] << 32) | + ((uint64_t)self->bytes[2] << 24) | + ((uint64_t)self->bytes[3] << 16) | + ((uint64_t)self->bytes[4] << 8) | + ((uint64_t)self->bytes[5]); + return PyLong_FromUnsignedLongLong(unix_ts_ms); + } + else { + // UUID v1 and others: time_lo (32) | time_mid (16) | ver (4) + // | time_hi (12) | ... (64) + uint32_t time_lo = get_time_low(self); + uint16_t time_mid = get_time_mid(self); + uint16_t time_hi = ((uint16_t)(self->bytes[6] & 0x0f) << 8) | + ((uint16_t)self->bytes[7]); + + uint64_t time = ((uint64_t)time_hi << 48) | + ((uint64_t)time_mid << 32) | + (uint64_t)time_lo; + return PyLong_FromUnsignedLongLong(time); + } +} + +static PyObject * +Uuid_get_clock_seq(uuidobject *self, void *closure) +{ + // clock_seq_hi_variant (byte 8) & 0x3f, then clock_seq_low (byte 9) + uint16_t clock_seq = ((uint16_t)(get_clock_seq_hi_variant(self) & 0x3f) << 8) | + ((uint16_t)get_clock_seq_low(self)); + return PyLong_FromUnsignedLong(clock_seq); +} + +static PyObject * +Uuid_get_node(uuidobject *self, void *closure) +{ + return PyLong_FromUnsignedLongLong(get_node(self)); +} + +static PyObject * +Uuid_get_bytes(uuidobject *self, void *closure) +{ + return PyBytes_FromStringAndSize((const char *)self->bytes, 16); +} + +static PyObject * +Uuid_get_bytes_le(uuidobject *self, void *closure) +{ + // UUID fields in little-endian order need to be byte-swapped: + // - time_low (4 bytes) - reversed + // - time_mid (2 bytes) - reversed + // - time_hi_version (2 bytes) - reversed + // - clock_seq and node (8 bytes) - unchanged + + unsigned char bytes_le[16]; + + // Reverse time_low (bytes 0-3) + bytes_le[0] = self->bytes[3]; + bytes_le[1] = self->bytes[2]; + bytes_le[2] = self->bytes[1]; + bytes_le[3] = self->bytes[0]; + + // Reverse time_mid (bytes 4-5) + bytes_le[4] = self->bytes[5]; + bytes_le[5] = self->bytes[4]; + + // Reverse time_hi_version (bytes 6-7) + bytes_le[6] = self->bytes[7]; + bytes_le[7] = self->bytes[6]; + + // Copy clock_seq and node as-is (bytes 8-15) + memcpy(bytes_le + 8, self->bytes + 8, 8); + + return PyBytes_FromStringAndSize((const char *)bytes_le, 16); +} + +static PyObject * +Uuid_get_fields(uuidobject *self, void *closure) +{ + uint32_t time_low = get_time_low(self); + uint16_t time_mid = get_time_mid(self); + uint16_t time_hi_version = get_time_hi_version(self); + uint8_t clock_seq_hi_variant = get_clock_seq_hi_variant(self); + uint8_t clock_seq_low = get_clock_seq_low(self); + uint64_t node = get_node(self); + + // Build and return the tuple + return Py_BuildValue( + "(kHHBBK)", + (unsigned long)time_low, + (unsigned short)time_mid, + (unsigned short)time_hi_version, + (unsigned char)clock_seq_hi_variant, + (unsigned char)clock_seq_low, + (unsigned long long)node + ); +} + +static PyObject * +uuid_from_bytes_array(PyTypeObject *type, uint8_t bytes[16]) +{ + uuidobject *self = make_uuid(type); + if (self == NULL) { + return NULL; + } + memcpy(self->bytes, bytes, 16); + return (PyObject *)self; +} + +static PyObject * +Uuid_nb_int(PyObject *self) +{ + return get_int((uuidobject *)self); +} + +static PyObject * +Uuid_richcompare(PyObject *self, PyObject *other, int op) +{ + uuid_state *state = get_uuid_state_by_cls(Py_TYPE(self)); + + if (!PyObject_TypeCheck(other, state->UuidType)) { + Py_RETURN_NOTIMPLEMENTED; + } + + uuidobject *uuid_self = (uuidobject *)self; + uuidobject *uuid_other = (uuidobject *)other; + + int cmp = memcmp(uuid_self->bytes, uuid_other->bytes, 16); + + int result; + switch (op) { + case Py_EQ: + result = (cmp == 0); + break; + case Py_NE: + result = (cmp != 0); + break; + case Py_LT: + result = (cmp < 0); + break; + case Py_LE: + result = (cmp <= 0); + break; + case Py_GT: + result = (cmp > 0); + break; + case Py_GE: + result = (cmp >= 0); + break; + default: + Py_RETURN_NOTIMPLEMENTED; + } + + if (result) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } +} + +static PyObject * +Uuid_str(PyObject *self) +{ + uuidobject *uuid = (uuidobject *)self; + + // UUID string format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (36 chars) + char str[36]; + + for (int i = 0; i < 4; i++) { + byte_to_hex(uuid->bytes[i], &str[i * 2]); + } + str[8] = '-'; + + for (int i = 4; i < 6; i++) { + byte_to_hex(uuid->bytes[i], &str[9 + (i - 4) * 2]); + } + str[13] = '-'; + + for (int i = 6; i < 8; i++) { + byte_to_hex(uuid->bytes[i], &str[14 + (i - 6) * 2]); + } + str[18] = '-'; + + for (int i = 8; i < 10; i++) { + byte_to_hex(uuid->bytes[i], &str[19 + (i - 8) * 2]); + } + str[23] = '-'; + + for (int i = 10; i < 16; i++) { + byte_to_hex(uuid->bytes[i], &str[24 + (i - 10) * 2]); + } + + return PyUnicode_FromStringAndSize(str, 36); +} + +static PyObject * +Uuid_repr(PyObject *self) +{ + PyObject *str_obj = Uuid_str(self); + if (str_obj == NULL) { + return NULL; + } + + // Get the class name (sadly can't use tp_name -- we don't need the full name) + PyObject *cls_name = PyObject_GetAttrString((PyObject *)Py_TYPE(self), "__name__"); + if (cls_name == NULL) { + Py_DECREF(str_obj); + return NULL; + } + + PyObject *repr = PyUnicode_FromFormat("%U('%U')", cls_name, str_obj); + Py_DECREF(str_obj); + Py_DECREF(cls_name); + return repr; +} + +static int +Uuid_setattr(PyObject *self, PyObject *name, PyObject *value) +{ + PyErr_SetString(PyExc_TypeError, "UUID objects are immutable"); + return -1; +} + +static PyObject * +Uuid_get_urn(uuidobject *self, void *closure) +{ + PyObject *str_obj = Uuid_str((PyObject *)self); + if (str_obj == NULL) { + return NULL; + } + + PyObject *urn = PyUnicode_FromFormat("urn:uuid:%U", str_obj); + Py_DECREF(str_obj); + return urn; +} + +static Py_hash_t +Uuid_hash(PyObject *self) +{ + uuidobject *uuid = (uuidobject *)self; + if (uuid->cached_hash != -1) { + // UUIDs are very often used in dicts/sets, so it makes + // sense to cache the computed hash (like we do for str) + return uuid->cached_hash; + } + + Py_hash_t hash = Py_HashBuffer(uuid->bytes, 16); + uuid->cached_hash = hash; + return hash; +} + + +/*[clinic input] +@classmethod +uuid.UUID._from_int + + value: object + / + +Create a UUID from an integer value. Internal use only. +[clinic start generated code]*/ + +static PyObject * +_uuid_UUID__from_int_impl(PyTypeObject *type, PyObject *value) +/*[clinic end generated code: output=05af0cfa4805fcae input=3f472ebfd07bbf50]*/ +{ + uuid_state *state = get_uuid_state_by_cls(type); + + if (validate_int(state, value) < 0) { + return NULL; + } + + uuidobject *self = make_uuid(type); + if (self == NULL) { + return NULL; + } + + if (from_int(self, value, 0) < 0) { + // We validated before creating an instance, so now we don't need to + // validate again + return NULL; + } + + return (PyObject *)self; +} + +static PyGetSetDef Uuid_getset[] = { + {"int", (getter)Uuid_get_int, NULL, "UUID as a 128-bit integer", NULL}, + {"is_safe", (getter)Uuid_get_is_safe, NULL, "UUID safety status", NULL}, + {"fields", (getter)Uuid_get_fields, NULL, "UUID as a 6-tuple", NULL}, + {"hex", (getter)Uuid_get_hex, NULL, "UUID as a 32-character hex string", NULL}, + {"urn", (getter)Uuid_get_urn, NULL, "UUID as a URN", NULL}, + {"variant", (getter)Uuid_get_variant, NULL, "UUID variant", NULL}, + {"version", (getter)Uuid_get_version, NULL, "UUID version", NULL}, + {"time_low", (getter)Uuid_get_time_low, NULL, "Time low field (32 bits)", NULL}, + {"time_mid", (getter)Uuid_get_time_mid, NULL, "Time mid field (16 bits)", NULL}, + {"bytes", (getter)Uuid_get_bytes, NULL, "UUID as a 16-byte string", NULL}, + {"bytes_le", (getter)Uuid_get_bytes_le, NULL, + "UUID as a 16-byte string in little-endian byte order", NULL}, + {"time_hi_version", (getter)Uuid_get_time_hi_version, NULL, + "Time high and version field (16 bits)", NULL}, + {"clock_seq_hi_variant", (getter)Uuid_get_clock_seq_hi_variant, NULL, + "Clock sequence high and variant field (8 bits)", NULL}, + {"clock_seq_low", (getter)Uuid_get_clock_seq_low, NULL, + "Clock sequence low field (8 bits)", NULL}, + {"time", (getter)Uuid_get_time, NULL, + "Time field (60 bits for v1/v6, 48 bits for v7)", NULL}, + {"clock_seq", (getter)Uuid_get_clock_seq, NULL, + "Clock sequence field (14 bits)", NULL}, + {"node", (getter)Uuid_get_node, NULL, + "Node field (48 bits)", NULL}, + {NULL} +}; + +/*[clinic input] +uuid.UUID.__getstate__ + +Return the UUID's state for pickling. +[clinic start generated code]*/ + +static PyObject * +_uuid_UUID___getstate___impl(uuidobject *self) +/*[clinic end generated code: output=f9278a4d28ccac91 input=4b471ae24b705e8e]*/ +{ + PyObject *dict = PyDict_New(); + if (dict == NULL) { + return NULL; + } + + // Always add 'int' key + PyObject *int_value = get_int(self); + if (int_value == NULL) { + Py_DECREF(dict); + return NULL; + } + if (PyDict_SetItemString(dict, "int", int_value) < 0) { + Py_DECREF(int_value); + Py_DECREF(dict); + return NULL; + } + Py_DECREF(int_value); + + if (PyDict_SetItemString(dict, "is_safe", self->is_safe) < 0) { + Py_DECREF(dict); + return NULL; + } + + return dict; +} + +/*[clinic input] +uuid.UUID.__setstate__ + + state: object + / + +Restore the UUID's state from pickling. + +Expects a dictionary with 'int' and optionally 'is_safe' keys. +[clinic start generated code]*/ + +static PyObject * +_uuid_UUID___setstate___impl(uuidobject *self, PyObject *state) +/*[clinic end generated code: output=cdf6bd4a2a680b3f input=b1ec0744788a73a0]*/ +{ + uuid_state *module_state = get_uuid_state_by_cls(Py_TYPE(self)); + + if (!PyDict_Check(state)) { + PyErr_SetString(PyExc_TypeError, "state must be a dictionary"); + return NULL; + } + + // Get and set the 'int' value + PyObject *int_value = PyDict_GetItemString(state, "int"); + if (int_value == NULL) { + PyErr_SetString(PyExc_ValueError, "state must have 'int' key"); + return NULL; + } + + if (from_int(self, int_value, 1) < 0) { + return NULL; + } + + // Get and set 'is_safe' if present + PyObject *is_safe = PyDict_GetItemString(state, "is_safe"); + if (is_safe != NULL) { + // is_safe is the integer value, we need to call SafeUUID(value) + PyObject *safe_uuid_member = PyObject_CallOneArg(module_state->safe_uuid, is_safe); + if (safe_uuid_member == NULL) { + return NULL; + } + Py_XDECREF(self->is_safe); + self->is_safe = safe_uuid_member; + } else { + // No is_safe in state, set to SafeUUID.unknown + Py_XDECREF(self->is_safe); + self->is_safe = Py_NewRef(module_state->safe_uuid_unknown); + } + + Py_RETURN_NONE; +} + +static PyMethodDef Uuid_methods[] = { + _UUID_UUID__FROM_INT_METHODDEF + _UUID_UUID___GETSTATE___METHODDEF + _UUID_UUID___SETSTATE___METHODDEF + {NULL, NULL} +}; + +static PyMemberDef Uuid_members[] = { + {"__weaklistoffset__", Py_T_PYSSIZET, offsetof(uuidobject, weakreflist), Py_READONLY}, + {NULL} +}; + +static PyType_Slot Uuid_slots[] = { + {Py_tp_alloc, Uuid_alloc}, + {Py_tp_dealloc, Uuid_dealloc}, + {Py_tp_getattro, PyObject_GenericGetAttr}, + {Py_tp_setattro, Uuid_setattr}, + {Py_tp_getset, Uuid_getset}, + {Py_tp_members, Uuid_members}, + {Py_tp_init, _uuid_UUID___init__}, + {Py_tp_doc, (void *)_uuid_UUID___init____doc__}, + {Py_tp_str, Uuid_str}, + {Py_tp_repr, Uuid_repr}, + {Py_tp_hash, Uuid_hash}, + {Py_tp_richcompare, Uuid_richcompare}, + {Py_nb_int, Uuid_nb_int}, + {Py_tp_methods, Uuid_methods}, + {0, NULL}, +}; + + +static PyType_Spec Uuid_spec = { + .name = "uuid.UUID", + .basicsize = sizeof(uuidobject), + .flags = ( + Py_TPFLAGS_DEFAULT + | Py_TPFLAGS_BASETYPE + | Py_TPFLAGS_HEAPTYPE + | Py_TPFLAGS_IMMUTABLETYPE + ), + .slots = Uuid_slots, +}; + + +static int +module_traverse(PyObject *mod, visitproc visit, void *arg) +{ + uuid_state *state = get_uuid_state(mod); + Py_VISIT(state->UuidType); + Py_VISIT(state->safe_uuid); + Py_VISIT(state->safe_uuid_safe); + Py_VISIT(state->safe_uuid_unsafe); + Py_VISIT(state->safe_uuid_unknown); + Py_VISIT(state->uint128_max); + Py_VISIT(state->reserved_ncs); + Py_VISIT(state->rfc_4122); + Py_VISIT(state->reserved_microsoft); + Py_VISIT(state->reserved_future); + return 0; +} + +static int +module_clear(PyObject *mod) +{ + uuid_state *state = get_uuid_state(mod); + + Py_CLEAR(state->UuidType); + Py_CLEAR(state->safe_uuid); + Py_CLEAR(state->safe_uuid_safe); + Py_CLEAR(state->safe_uuid_unsafe); + Py_CLEAR(state->safe_uuid_unknown); + Py_CLEAR(state->uint128_max); + Py_CLEAR(state->reserved_ncs); + Py_CLEAR(state->rfc_4122); + Py_CLEAR(state->reserved_microsoft); + Py_CLEAR(state->reserved_future); + + if (state->freelist != NULL) { + while (state->freelist != NULL) { + uuidobject *cur = state->freelist; + state->freelist = (uuidobject *)cur->weakreflist; + PyObject_Free(cur); + } + state->freelist = NULL; + state->freelist_size = 0; + } + + return 0; +} + +static void +module_free(void *mod) +{ + (void)module_clear((PyObject *)mod); +} + + static int uuid_exec(PyObject *module) { + uuid_state *state = get_uuid_state(module); + PyObject *uuid_mod = NULL; + PyObject *safe_uuid = NULL; + #define ADD_INT(NAME, VALUE) \ do { \ if (PyModule_AddIntConstant(module, (NAME), (VALUE)) < 0) { \ - return -1; \ + goto fail; \ } \ } while (0) @@ -122,17 +1622,93 @@ uuid_exec(PyObject *module) #endif #undef ADD_INT + + state->UuidType = (PyTypeObject *)PyType_FromMetaclass( + NULL, + module, + &Uuid_spec, + NULL + ); + if (state->UuidType == NULL) { + goto fail; + } + if (PyModule_AddType(module, state->UuidType) < 0) { + goto fail; + } + + uuid_mod = PyImport_ImportModule("uuid"); + if (uuid_mod == NULL) { + goto fail; + } + safe_uuid = state->safe_uuid = PyObject_GetAttrString(uuid_mod, "SafeUUID"); + if (safe_uuid == NULL) { + goto fail; + } + state->safe_uuid_safe = PyObject_GetAttrString(safe_uuid, "safe"); + if (state->safe_uuid_safe == NULL) { + goto fail; + } + state->safe_uuid_unsafe = PyObject_GetAttrString(safe_uuid, "unsafe"); + if (state->safe_uuid_unsafe == NULL) { + goto fail; + } + state->safe_uuid_unknown = PyObject_GetAttrString(safe_uuid, "unknown"); + if (state->safe_uuid_unknown == NULL) { + goto fail; + } + + state->uint128_max = PyObject_GetAttrString(uuid_mod, "_UINT_128_MAX"); + if (state->uint128_max == NULL) { + goto fail; + } + + state->reserved_ncs = PyObject_GetAttrString(uuid_mod, "RESERVED_NCS"); + if (state->reserved_ncs == NULL) { + goto fail; + } + + state->rfc_4122 = PyObject_GetAttrString(uuid_mod, "RFC_4122"); + if (state->rfc_4122 == NULL) { + goto fail; + } + + state->reserved_microsoft = PyObject_GetAttrString(uuid_mod, "RESERVED_MICROSOFT"); + if (state->reserved_microsoft == NULL) { + goto fail; + } + + state->reserved_future = PyObject_GetAttrString(uuid_mod, "RESERVED_FUTURE"); + if (state->reserved_future == NULL) { + goto fail; + } + + state->last_timestamp_v7 = 0; + state->last_counter_v7 = 0; + state->random_last_pid = uuid_getpid(); + + state->freelist = NULL; + state->freelist_size = 0; + + state->random_idx = RANDOM_BUF_SIZE; + + Py_CLEAR(uuid_mod); return 0; + +fail: + Py_CLEAR(uuid_mod); + return -1; } static PyMethodDef uuid_methods[] = { + _UUID_UUID4_METHODDEF + _UUID_UUID7_METHODDEF #if defined(HAVE_UUID_UUID_H) || defined(HAVE_UUID_H) {"generate_time_safe", py_uuid_generate_time_safe, METH_NOARGS, NULL}, #endif #if defined(MS_WINDOWS) {"UuidCreate", py_UuidCreate, METH_NOARGS, NULL}, #endif - {NULL, NULL, 0, NULL} /* sentinel */ + {NULL, NULL, 0, NULL} }; static PyModuleDef_Slot uuid_slots[] = { @@ -145,9 +1721,12 @@ static PyModuleDef_Slot uuid_slots[] = { static struct PyModuleDef uuidmodule = { PyModuleDef_HEAD_INIT, .m_name = "_uuid", - .m_size = 0, + .m_size = sizeof(uuid_state), .m_methods = uuid_methods, + .m_traverse = module_traverse, + .m_clear = module_clear, .m_slots = uuid_slots, + .m_free = module_free, }; PyMODINIT_FUNC diff --git a/Modules/clinic/_uuidmodule.c.h b/Modules/clinic/_uuidmodule.c.h new file mode 100644 index 000000000000000..3d9c374d83087d5 --- /dev/null +++ b/Modules/clinic/_uuidmodule.c.h @@ -0,0 +1,255 @@ +/*[clinic input] +preserve +[clinic start generated code]*/ + +#if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) +# include "pycore_gc.h" // PyGC_Head +# include "pycore_runtime.h" // _Py_ID() +#endif +#include "pycore_critical_section.h"// Py_BEGIN_CRITICAL_SECTION() +#include "pycore_modsupport.h" // _PyArg_UnpackKeywords() + +PyDoc_STRVAR(_uuid_uuid4__doc__, +"uuid4($module, /)\n" +"--\n" +"\n" +"Generate a random UUID (version 4)."); + +#define _UUID_UUID4_METHODDEF \ + {"uuid4", (PyCFunction)_uuid_uuid4, METH_NOARGS, _uuid_uuid4__doc__}, + +static PyObject * +_uuid_uuid4_impl(PyObject *module); + +static PyObject * +_uuid_uuid4(PyObject *module, PyObject *Py_UNUSED(ignored)) +{ + PyObject *return_value = NULL; + + Py_BEGIN_CRITICAL_SECTION(module); + return_value = _uuid_uuid4_impl(module); + Py_END_CRITICAL_SECTION(); + + return return_value; +} + +PyDoc_STRVAR(_uuid_uuid7__doc__, +"uuid7($module, /)\n" +"--\n" +"\n" +"Generate a UUID from a Unix timestamp in milliseconds and random bits.\n" +"\n" +"UUIDv7 objects feature monotonicity within a millisecond."); + +#define _UUID_UUID7_METHODDEF \ + {"uuid7", (PyCFunction)_uuid_uuid7, METH_NOARGS, _uuid_uuid7__doc__}, + +static PyObject * +_uuid_uuid7_impl(PyObject *module); + +static PyObject * +_uuid_uuid7(PyObject *module, PyObject *Py_UNUSED(ignored)) +{ + PyObject *return_value = NULL; + + Py_BEGIN_CRITICAL_SECTION(module); + return_value = _uuid_uuid7_impl(module); + Py_END_CRITICAL_SECTION(); + + return return_value; +} + +PyDoc_STRVAR(_uuid_UUID___init____doc__, +"UUID(hex=, bytes=None, bytes_le=None,\n" +" fields=, int=,\n" +" version=, *, is_safe=)\n" +"--\n" +"\n" +"UUID is a fast base implementation type for uuid.UUID."); + +static int +_uuid_UUID___init___impl(uuidobject *self, PyObject *hex, Py_buffer *bytes, + Py_buffer *bytes_le, PyObject *fields, + PyObject *int_value, PyObject *version, + PyObject *is_safe); + +static int +_uuid_UUID___init__(PyObject *self, PyObject *args, PyObject *kwargs) +{ + int return_value = -1; + #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) + + #define NUM_KEYWORDS 7 + static struct { + PyGC_Head _this_is_not_used; + PyObject_VAR_HEAD + Py_hash_t ob_hash; + PyObject *ob_item[NUM_KEYWORDS]; + } _kwtuple = { + .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) + .ob_hash = -1, + .ob_item = { &_Py_ID(hex), &_Py_ID(bytes), &_Py_ID(bytes_le), &_Py_ID(fields), &_Py_ID(int), &_Py_ID(version), &_Py_ID(is_safe), }, + }; + #undef NUM_KEYWORDS + #define KWTUPLE (&_kwtuple.ob_base.ob_base) + + #else // !Py_BUILD_CORE + # define KWTUPLE NULL + #endif // !Py_BUILD_CORE + + static const char * const _keywords[] = {"hex", "bytes", "bytes_le", "fields", "int", "version", "is_safe", NULL}; + static _PyArg_Parser _parser = { + .keywords = _keywords, + .fname = "UUID", + .kwtuple = KWTUPLE, + }; + #undef KWTUPLE + PyObject *argsbuf[7]; + PyObject * const *fastargs; + Py_ssize_t nargs = PyTuple_GET_SIZE(args); + Py_ssize_t noptargs = nargs + (kwargs ? PyDict_GET_SIZE(kwargs) : 0) - 0; + PyObject *hex = NULL; + Py_buffer bytes = {NULL, NULL}; + Py_buffer bytes_le = {NULL, NULL}; + PyObject *fields = NULL; + PyObject *int_value = NULL; + PyObject *version = NULL; + PyObject *is_safe = NULL; + + fastargs = _PyArg_UnpackKeywords(_PyTuple_CAST(args)->ob_item, nargs, kwargs, NULL, &_parser, + /*minpos*/ 0, /*maxpos*/ 6, /*minkw*/ 0, /*varpos*/ 0, argsbuf); + if (!fastargs) { + goto exit; + } + if (!noptargs) { + goto skip_optional_pos; + } + if (fastargs[0]) { + if (!PyUnicode_Check(fastargs[0])) { + _PyArg_BadArgument("UUID", "argument 'hex'", "str", fastargs[0]); + goto exit; + } + hex = fastargs[0]; + if (!--noptargs) { + goto skip_optional_pos; + } + } + if (fastargs[1]) { + if (PyObject_GetBuffer(fastargs[1], &bytes, PyBUF_SIMPLE) != 0) { + goto exit; + } + if (!--noptargs) { + goto skip_optional_pos; + } + } + if (fastargs[2]) { + if (PyObject_GetBuffer(fastargs[2], &bytes_le, PyBUF_SIMPLE) != 0) { + goto exit; + } + if (!--noptargs) { + goto skip_optional_pos; + } + } + if (fastargs[3]) { + fields = fastargs[3]; + if (!--noptargs) { + goto skip_optional_pos; + } + } + if (fastargs[4]) { + int_value = fastargs[4]; + if (!--noptargs) { + goto skip_optional_pos; + } + } + if (fastargs[5]) { + version = fastargs[5]; + if (!--noptargs) { + goto skip_optional_pos; + } + } +skip_optional_pos: + if (!noptargs) { + goto skip_optional_kwonly; + } + is_safe = fastargs[6]; +skip_optional_kwonly: + return_value = _uuid_UUID___init___impl((uuidobject *)self, hex, &bytes, &bytes_le, fields, int_value, version, is_safe); + +exit: + /* Cleanup for bytes */ + if (bytes.obj) { + PyBuffer_Release(&bytes); + } + /* Cleanup for bytes_le */ + if (bytes_le.obj) { + PyBuffer_Release(&bytes_le); + } + + return return_value; +} + +PyDoc_STRVAR(_uuid_UUID__from_int__doc__, +"_from_int($type, value, /)\n" +"--\n" +"\n" +"Create a UUID from an integer value. Internal use only."); + +#define _UUID_UUID__FROM_INT_METHODDEF \ + {"_from_int", (PyCFunction)_uuid_UUID__from_int, METH_O|METH_CLASS, _uuid_UUID__from_int__doc__}, + +static PyObject * +_uuid_UUID__from_int_impl(PyTypeObject *type, PyObject *value); + +static PyObject * +_uuid_UUID__from_int(PyObject *type, PyObject *value) +{ + PyObject *return_value = NULL; + + return_value = _uuid_UUID__from_int_impl((PyTypeObject *)type, value); + + return return_value; +} + +PyDoc_STRVAR(_uuid_UUID___getstate____doc__, +"__getstate__($self, /)\n" +"--\n" +"\n" +"Return the UUID\'s state for pickling."); + +#define _UUID_UUID___GETSTATE___METHODDEF \ + {"__getstate__", (PyCFunction)_uuid_UUID___getstate__, METH_NOARGS, _uuid_UUID___getstate____doc__}, + +static PyObject * +_uuid_UUID___getstate___impl(uuidobject *self); + +static PyObject * +_uuid_UUID___getstate__(PyObject *self, PyObject *Py_UNUSED(ignored)) +{ + return _uuid_UUID___getstate___impl((uuidobject *)self); +} + +PyDoc_STRVAR(_uuid_UUID___setstate____doc__, +"__setstate__($self, state, /)\n" +"--\n" +"\n" +"Restore the UUID\'s state from pickling.\n" +"\n" +"Expects a dictionary with \'int\' and optionally \'is_safe\' keys."); + +#define _UUID_UUID___SETSTATE___METHODDEF \ + {"__setstate__", (PyCFunction)_uuid_UUID___setstate__, METH_O, _uuid_UUID___setstate____doc__}, + +static PyObject * +_uuid_UUID___setstate___impl(uuidobject *self, PyObject *state); + +static PyObject * +_uuid_UUID___setstate__(PyObject *self, PyObject *state) +{ + PyObject *return_value = NULL; + + return_value = _uuid_UUID___setstate___impl((uuidobject *)self, state); + + return return_value; +} +/*[clinic end generated code: output=ec50cafa0e028d2b input=a9049054013a1b77]*/ From d80180d490c1031581c97f5c0a43498d3379a496 Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Fri, 19 Sep 2025 12:19:58 +0100 Subject: [PATCH 02/38] Address @picnixz's review --- Modules/_uuidmodule.c | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/Modules/_uuidmodule.c b/Modules/_uuidmodule.c index e1554e0ffec7f35..0ed90ec6e7e306f 100644 --- a/Modules/_uuidmodule.c +++ b/Modules/_uuidmodule.c @@ -1,4 +1,13 @@ -// UUID accelerator base type. +/* + * Python UUID module: + * - wraps libuuid or Windows rpcrt4.dll. + * - implements fast version of the uuid.py:UUID class. + * - re-implements uuid4() and uuid7() functions with improved performance + * by virtue of them being implemented in C and better entropy fetching + * strategy. + * + * DCE compatible Universally Unique Identifier library. + */ #ifndef Py_BUILD_CORE_BUILTIN # define Py_BUILD_CORE_MODULE 1 @@ -140,6 +149,8 @@ typedef struct uuidobject { // Bytes 8-9: clock_seq_and_variant (16 bits) // Bytes 10-15: node (48 bits) // +// Note that the time attributes are only relevant to versions 1, 6 and 7. +// // Version field is located in byte 6; most significant 4 bits: // // Variant field is located in byte 8; most significant variable bits: @@ -489,8 +500,7 @@ _uuid_UUID___init___impl(uuidobject *self, PyObject *hex, Py_buffer *bytes, if (validated == NULL) { return -1; } - Py_CLEAR(self->is_safe); - self->is_safe = validated; // reuse reference + Py_XSETREF(self->is_safe, validated); } return 0; @@ -737,12 +747,12 @@ extract_field( int overflow; uint64_t value = PyLong_AsLongLongAndOverflow(field, &overflow); if (overflow || (value == (uint64_t)-1 && PyErr_Occurred())) { - PyErr_Format(PyExc_ValueError, "%s", error_msg); + PyErr_SetString(PyExc_ValueError, error_msg); goto fail; } if (value > max_value) { - PyErr_Format(PyExc_ValueError, "%s", error_msg); + PyErr_SetString(PyExc_ValueError, error_msg); goto fail; } @@ -770,14 +780,16 @@ from_fields(uuidobject *self, PyObject *fields) return -1; } - #define EXTRACT_FIELD(field_num, max_value, error_msg, type, name) \ +# define EXTRACT_FIELD(field_num, max_value, error_msg, type, name) \ type name; \ - uint64_t name##_extracted; \ - if (extract_field(fields, field_num, max_value, error_msg, \ - &(name##_extracted)) < 0) { \ - return -1; \ - } \ - name = (type)name##_extracted; + do { \ + uint64_t name##_extracted; \ + if (extract_field(fields, field_num, max_value, error_msg, \ + &(name##_extracted)) < 0) { \ + return -1; \ + } \ + name = (type)name##_extracted; \ + } while(0) EXTRACT_FIELD( 0, (1ULL << 32) - 1, "field 1 out of range (need a 32-bit value)", From ff3e3179e3851607e6a19209e214684d837c2015 Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Fri, 19 Sep 2025 12:27:11 +0100 Subject: [PATCH 03/38] Regen the clinic files --- Modules/_uuidmodule.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Modules/_uuidmodule.c b/Modules/_uuidmodule.c index 0ed90ec6e7e306f..d6c5d2d9fd3b965 100644 --- a/Modules/_uuidmodule.c +++ b/Modules/_uuidmodule.c @@ -196,7 +196,7 @@ typedef struct { #include "clinic/_uuidmodule.c.h" /*[clinic input] -class uuid.UUID "uuidobject *" "&UuidType" +class _uuid.UUID "uuidobject *" "&UuidType" [clinic start generated code]*/ /*[clinic end generated code: output=da39a3ee5e6b4b0d input=84ae6e2089cffd3f]*/ @@ -402,7 +402,7 @@ _uuid_uuid7_impl(PyObject *module) } /*[clinic input] -uuid.UUID.__init__ +_uuid.UUID.__init__ hex: 'U' = NULL bytes: 'y*' = None @@ -1357,7 +1357,7 @@ Uuid_hash(PyObject *self) /*[clinic input] @classmethod -uuid.UUID._from_int +_uuid.UUID._from_int value: object / @@ -1418,7 +1418,7 @@ static PyGetSetDef Uuid_getset[] = { }; /*[clinic input] -uuid.UUID.__getstate__ +_uuid.UUID.__getstate__ Return the UUID's state for pickling. [clinic start generated code]*/ @@ -1454,7 +1454,7 @@ _uuid_UUID___getstate___impl(uuidobject *self) } /*[clinic input] -uuid.UUID.__setstate__ +_uuid.UUID.__setstate__ state: object / From a7e9690b7f3109086f622c1a30a1c412887d4bd3 Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Fri, 19 Sep 2025 12:29:18 +0100 Subject: [PATCH 04/38] Clarify the type name --- Modules/_uuidmodule.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Modules/_uuidmodule.c b/Modules/_uuidmodule.c index d6c5d2d9fd3b965..17ecc509bf26ae2 100644 --- a/Modules/_uuidmodule.c +++ b/Modules/_uuidmodule.c @@ -1537,7 +1537,13 @@ static PyType_Slot Uuid_slots[] = { static PyType_Spec Uuid_spec = { + // We use "uuid.UUID" here and not "_uuid.UUID" to have full + // compatibility with old pickled UUIDs. There's no workaround + // if we want both to produce compatible pickles that can be read + // by older Pythons (using ancient pickle protocol verions) and + // restore from pickles produced by old Python versions. .name = "uuid.UUID", + .basicsize = sizeof(uuidobject), .flags = ( Py_TPFLAGS_DEFAULT From c1e529090e47fd1b36e59d1f1c4a99c5b5348464 Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Fri, 19 Sep 2025 13:05:05 +0100 Subject: [PATCH 05/38] Fix news --- .../next/Library/2025-09-18-14-13-00.gh-issue-139122.m3lp66.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2025-09-18-14-13-00.gh-issue-139122.m3lp66.rst b/Misc/NEWS.d/next/Library/2025-09-18-14-13-00.gh-issue-139122.m3lp66.rst index 72eb12fecf9853e..243d91112f40774 100644 --- a/Misc/NEWS.d/next/Library/2025-09-18-14-13-00.gh-issue-139122.m3lp66.rst +++ b/Misc/NEWS.d/next/Library/2025-09-18-14-13-00.gh-issue-139122.m3lp66.rst @@ -25,7 +25,7 @@ Summary of changes: Python implementation fully. * The Python implementation stores UUID values as int objects. The C - implementation stores them as `uint8_t[16]` array. + implementation stores them as ``uint8_t[16]`` array. * The C implementation has faster hash() implementation but also caches the computed hash value to speedup cases when UUIDs are used as From 50fb64a62a46dfe8816db927c07171590d9889aa Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Fri, 19 Sep 2025 13:54:21 +0100 Subject: [PATCH 06/38] Trim the NEWS file down --- ...-09-18-14-13-00.gh-issue-139122.m3lp66.rst | 55 ------------------- 1 file changed, 55 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2025-09-18-14-13-00.gh-issue-139122.m3lp66.rst b/Misc/NEWS.d/next/Library/2025-09-18-14-13-00.gh-issue-139122.m3lp66.rst index 243d91112f40774..bc3fd369dd5566c 100644 --- a/Misc/NEWS.d/next/Library/2025-09-18-14-13-00.gh-issue-139122.m3lp66.rst +++ b/Misc/NEWS.d/next/Library/2025-09-18-14-13-00.gh-issue-139122.m3lp66.rst @@ -1,57 +1,2 @@ Reimplement base UUID type, uuid4(), and uuid7() in C -The C implementation considerably boosts the performance of the key UUID -operations: - ------------------------------------- -Operation Speedup ------------------------------------- -uuid4() generation 15.01x -uuid7() generation 29.64x -UUID from string 6.76x -UUID from bytes 5.16x -str(uuid) conversion 6.66x ------------------------------------- - -Summary of changes: - -* The UUID type is reimplemented in C in its entirety. - -* The pure-Python is kept around and is used of the C implementation - isn't available for some reason. - -* Both implementations are tested extensively; additional tests are - added to ensure that the C implementation of the type follows the pure - Python implementation fully. - -* The Python implementation stores UUID values as int objects. The C - implementation stores them as ``uint8_t[16]`` array. - -* The C implementation has faster hash() implementation but also caches - the computed hash value to speedup cases when UUIDs are used as - set/dict keys. - -* The C implementation has a freelist to make new UUID object - instantiation as fast as possible. - -* uuid4() and uuid7() are now implmented in C. The most performance - boost (10x) comes from overfetching entropy to decrease the number of - _PyOS_URandom() calls. On its own it's a safe optimization with the - edge case that Unix fork needs to be explicitly handled. We do that by - comparing the current PID to the PID of when the random buffer was - populated. - -* Portions of code are coming from my implementation of faster UUID - in gel-python [1]. I did use AI during the development, but basically - had to rewrite the code it generated to be more idiomatic and - efficient. - -* The benchmark can be found here [2]. - -* This PR makes Python UUID operations as fast as they are in NodeJS and - Bun runtimes. - -[1] -https://github.com/MagicStack/py-pgproto/blob/b8109fb311a59f30f9947567a13508da9a776564/uuid.pyx - -[2] https://gist.github.com/1st1/f03e816f34a61e4d46c78ff98baf4818 From 62c3fda2bfb86f89c85bacd7922fa0b919905c57 Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Fri, 19 Sep 2025 14:03:15 +0100 Subject: [PATCH 07/38] Use PyObject* in getters --- Modules/_uuidmodule.c | 93 +++++++++++++++++++++++++------------------ 1 file changed, 54 insertions(+), 39 deletions(-) diff --git a/Modules/_uuidmodule.c b/Modules/_uuidmodule.c index 17ecc509bf26ae2..e607ebf066da861 100644 --- a/Modules/_uuidmodule.c +++ b/Modules/_uuidmodule.c @@ -928,14 +928,15 @@ Uuid_dealloc(PyObject *obj) static PyObject * -Uuid_get_int(uuidobject *self, void *closure) +Uuid_get_int(PyObject *o, void *closure) { - return get_int(self); + return get_int((uuidobject *)o); } static PyObject * -Uuid_get_is_safe(uuidobject *self, void *closure) +Uuid_get_is_safe(PyObject *o, void *closure) { + uuidobject *self = (uuidobject *)o; if (self->is_safe == NULL) { Py_RETURN_NONE; } @@ -943,8 +944,9 @@ Uuid_get_is_safe(uuidobject *self, void *closure) } static PyObject * -Uuid_get_hex(uuidobject *self, void *closure) +Uuid_get_hex(PyObject *o, void *closure) { + uuidobject *self = (uuidobject *)o; char hex[32]; for (int i = 0; i < 16; i++) { byte_to_hex(self->bytes[i], &hex[i * 2]); @@ -953,8 +955,9 @@ Uuid_get_hex(uuidobject *self, void *closure) } static PyObject * -Uuid_get_variant(uuidobject *self, void *closure) +Uuid_get_variant(PyObject *o, void *closure) { + uuidobject *self = (uuidobject *)o; uuid_state *state = get_uuid_state_by_cls(Py_TYPE(self)); uint8_t variant_byte = self->bytes[8]; @@ -1000,8 +1003,9 @@ get_version(uuidobject *self) } static PyObject * -Uuid_get_version(uuidobject *self, void *closure) +Uuid_get_version(PyObject *o, void *closure) { + uuidobject *self = (uuidobject *)o; if (!is_rfc_4122(self)) { Py_RETURN_NONE; } @@ -1055,38 +1059,45 @@ get_node(uuidobject *self) } static PyObject * -Uuid_get_time_low(uuidobject *self, void *closure) +Uuid_get_time_low(PyObject *o, void *closure) { + uuidobject *self = (uuidobject *)o; return PyLong_FromUnsignedLong(get_time_low(self)); } static PyObject * -Uuid_get_time_mid(uuidobject *self, void *closure) +Uuid_get_time_mid(PyObject *o, void *closure) { + uuidobject *self = (uuidobject *)o; return PyLong_FromUnsignedLong(get_time_mid(self)); } static PyObject * -Uuid_get_time_hi_version(uuidobject *self, void *closure) +Uuid_get_time_hi_version(PyObject *o, void *closure) { + uuidobject *self = (uuidobject *)o; return PyLong_FromUnsignedLong(get_time_hi_version(self)); } static PyObject * -Uuid_get_clock_seq_hi_variant(uuidobject *self, void *closure) +Uuid_get_clock_seq_hi_variant(PyObject *o, void *closure) { + uuidobject *self = (uuidobject *)o; return PyLong_FromUnsignedLong(get_clock_seq_hi_variant(self)); } static PyObject * -Uuid_get_clock_seq_low(uuidobject *self, void *closure) +Uuid_get_clock_seq_low(PyObject *o, void *closure) { + uuidobject *self = (uuidobject *)o; return PyLong_FromUnsignedLong(get_clock_seq_low(self)); } static PyObject * -Uuid_get_time(uuidobject *self, void *closure) +Uuid_get_time(PyObject *o, void *closure) { + uuidobject *self = (uuidobject *)o; + long version = get_version(self); if (version == 6) { @@ -1128,8 +1139,9 @@ Uuid_get_time(uuidobject *self, void *closure) } static PyObject * -Uuid_get_clock_seq(uuidobject *self, void *closure) +Uuid_get_clock_seq(PyObject *o, void *closure) { + uuidobject *self = (uuidobject *)o; // clock_seq_hi_variant (byte 8) & 0x3f, then clock_seq_low (byte 9) uint16_t clock_seq = ((uint16_t)(get_clock_seq_hi_variant(self) & 0x3f) << 8) | ((uint16_t)get_clock_seq_low(self)); @@ -1137,20 +1149,23 @@ Uuid_get_clock_seq(uuidobject *self, void *closure) } static PyObject * -Uuid_get_node(uuidobject *self, void *closure) +Uuid_get_node(PyObject *o, void *closure) { + uuidobject *self = (uuidobject *)o; return PyLong_FromUnsignedLongLong(get_node(self)); } static PyObject * -Uuid_get_bytes(uuidobject *self, void *closure) +Uuid_get_bytes(PyObject *o, void *closure) { + uuidobject *self = (uuidobject *)o; return PyBytes_FromStringAndSize((const char *)self->bytes, 16); } static PyObject * -Uuid_get_bytes_le(uuidobject *self, void *closure) +Uuid_get_bytes_le(PyObject *o, void *closure) { + uuidobject *self = (uuidobject *)o; // UUID fields in little-endian order need to be byte-swapped: // - time_low (4 bytes) - reversed // - time_mid (2 bytes) - reversed @@ -1180,8 +1195,9 @@ Uuid_get_bytes_le(uuidobject *self, void *closure) } static PyObject * -Uuid_get_fields(uuidobject *self, void *closure) +Uuid_get_fields(PyObject *o, void *closure) { + uuidobject *self = (uuidobject *)o; uint32_t time_low = get_time_low(self); uint16_t time_mid = get_time_mid(self); uint16_t time_hi_version = get_time_hi_version(self); @@ -1327,8 +1343,9 @@ Uuid_setattr(PyObject *self, PyObject *name, PyObject *value) } static PyObject * -Uuid_get_urn(uuidobject *self, void *closure) +Uuid_get_urn(PyObject *o, void *closure) { + uuidobject *self = (uuidobject *)o; PyObject *str_obj = Uuid_str((PyObject *)self); if (str_obj == NULL) { return NULL; @@ -1340,9 +1357,9 @@ Uuid_get_urn(uuidobject *self, void *closure) } static Py_hash_t -Uuid_hash(PyObject *self) +Uuid_hash(PyObject *o) { - uuidobject *uuid = (uuidobject *)self; + uuidobject *uuid = (uuidobject *)o; if (uuid->cached_hash != -1) { // UUIDs are very often used in dicts/sets, so it makes // sense to cache the computed hash (like we do for str) @@ -1390,30 +1407,28 @@ _uuid_UUID__from_int_impl(PyTypeObject *type, PyObject *value) } static PyGetSetDef Uuid_getset[] = { - {"int", (getter)Uuid_get_int, NULL, "UUID as a 128-bit integer", NULL}, - {"is_safe", (getter)Uuid_get_is_safe, NULL, "UUID safety status", NULL}, - {"fields", (getter)Uuid_get_fields, NULL, "UUID as a 6-tuple", NULL}, - {"hex", (getter)Uuid_get_hex, NULL, "UUID as a 32-character hex string", NULL}, - {"urn", (getter)Uuid_get_urn, NULL, "UUID as a URN", NULL}, - {"variant", (getter)Uuid_get_variant, NULL, "UUID variant", NULL}, - {"version", (getter)Uuid_get_version, NULL, "UUID version", NULL}, - {"time_low", (getter)Uuid_get_time_low, NULL, "Time low field (32 bits)", NULL}, - {"time_mid", (getter)Uuid_get_time_mid, NULL, "Time mid field (16 bits)", NULL}, - {"bytes", (getter)Uuid_get_bytes, NULL, "UUID as a 16-byte string", NULL}, - {"bytes_le", (getter)Uuid_get_bytes_le, NULL, + {"int", Uuid_get_int, NULL, "UUID as a 128-bit integer", NULL}, + {"is_safe", Uuid_get_is_safe, NULL, "UUID safety status", NULL}, + {"fields", Uuid_get_fields, NULL, "UUID as a 6-tuple", NULL}, + {"hex", Uuid_get_hex, NULL, "UUID as a 32-character hex string", NULL}, + {"urn", Uuid_get_urn, NULL, "UUID as a URN", NULL}, + {"variant", Uuid_get_variant, NULL, "UUID variant", NULL}, + {"version", Uuid_get_version, NULL, "UUID version", NULL}, + {"time_low", Uuid_get_time_low, NULL, "Time low field (32 bits)", NULL}, + {"time_mid", Uuid_get_time_mid, NULL, "Time mid field (16 bits)", NULL}, + {"bytes", Uuid_get_bytes, NULL, "UUID as a 16-byte string", NULL}, + {"bytes_le", Uuid_get_bytes_le, NULL, "UUID as a 16-byte string in little-endian byte order", NULL}, - {"time_hi_version", (getter)Uuid_get_time_hi_version, NULL, + {"time_hi_version", Uuid_get_time_hi_version, NULL, "Time high and version field (16 bits)", NULL}, - {"clock_seq_hi_variant", (getter)Uuid_get_clock_seq_hi_variant, NULL, + {"clock_seq_hi_variant", Uuid_get_clock_seq_hi_variant, NULL, "Clock sequence high and variant field (8 bits)", NULL}, - {"clock_seq_low", (getter)Uuid_get_clock_seq_low, NULL, + {"clock_seq_low", Uuid_get_clock_seq_low, NULL, "Clock sequence low field (8 bits)", NULL}, - {"time", (getter)Uuid_get_time, NULL, + {"time", Uuid_get_time, NULL, "Time field (60 bits for v1/v6, 48 bits for v7)", NULL}, - {"clock_seq", (getter)Uuid_get_clock_seq, NULL, - "Clock sequence field (14 bits)", NULL}, - {"node", (getter)Uuid_get_node, NULL, - "Node field (48 bits)", NULL}, + {"clock_seq", Uuid_get_clock_seq, NULL, "Clock sequence field (14 bits)", NULL}, + {"node", Uuid_get_node, NULL, "Node field (48 bits)", NULL}, {NULL} }; From 22c0d61db204d509fc915d9f577219b89b20758a Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Fri, 19 Sep 2025 15:29:57 +0100 Subject: [PATCH 08/38] Codegen string literals and use them --- Include/internal/pycore_global_objects_fini_generated.h | 4 ++++ Include/internal/pycore_global_strings.h | 4 ++++ Include/internal/pycore_runtime_init_generated.h | 4 ++++ Include/internal/pycore_unicodeobject_generated.h | 4 ++++ Modules/_uuidmodule.c | 9 +++++---- 5 files changed, 21 insertions(+), 4 deletions(-) diff --git a/Include/internal/pycore_global_objects_fini_generated.h b/Include/internal/pycore_global_objects_fini_generated.h index e75376f008dccd8..b07e5c9894fe8fe 100644 --- a/Include/internal/pycore_global_objects_fini_generated.h +++ b/Include/internal/pycore_global_objects_fini_generated.h @@ -337,6 +337,7 @@ _PyStaticObjects_CheckAll(PyInterpreterState *interp) { _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(byte_size), "byte_size", 9); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(byteorder), "byteorder", 9); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(bytes), "bytes", 5); + _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(bytes_le), "bytes_le", 8); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(bytes_per_sep), "bytes_per_sep", 13); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(c_call), "c_call", 6); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(c_exception), "c_exception", 11); @@ -531,6 +532,7 @@ _PyStaticObjects_CheckAll(PyInterpreterState *interp) { _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(hash_name), "hash_name", 9); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(header), "header", 6); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(headers), "headers", 7); + _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(hex), "hex", 3); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(hi), "hi", 2); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(hook), "hook", 4); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(host), "host", 4); @@ -564,6 +566,7 @@ _PyStaticObjects_CheckAll(PyInterpreterState *interp) { _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(insert_comments), "insert_comments", 15); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(insert_pis), "insert_pis", 10); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(instructions), "instructions", 12); + _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(int), "int", 3); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(intern), "intern", 6); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(intersection), "intersection", 12); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(interval), "interval", 8); @@ -571,6 +574,7 @@ _PyStaticObjects_CheckAll(PyInterpreterState *interp) { _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(is_compress), "is_compress", 11); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(is_raw), "is_raw", 6); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(is_running), "is_running", 10); + _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(is_safe), "is_safe", 7); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(is_struct), "is_struct", 9); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(isatty), "isatty", 6); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(isinstance), "isinstance", 10); diff --git a/Include/internal/pycore_global_strings.h b/Include/internal/pycore_global_strings.h index 467a108e119995e..fedde521c838484 100644 --- a/Include/internal/pycore_global_strings.h +++ b/Include/internal/pycore_global_strings.h @@ -349,6 +349,7 @@ struct _Py_global_strings { STRUCT_FOR_ID(byte_size) STRUCT_FOR_ID(byteorder) STRUCT_FOR_ID(bytes) + STRUCT_FOR_ID(bytes_le) STRUCT_FOR_ID(bytes_per_sep) STRUCT_FOR_ID(c_call) STRUCT_FOR_ID(c_exception) @@ -543,6 +544,7 @@ struct _Py_global_strings { STRUCT_FOR_ID(hash_name) STRUCT_FOR_ID(header) STRUCT_FOR_ID(headers) + STRUCT_FOR_ID(hex) STRUCT_FOR_ID(hi) STRUCT_FOR_ID(hook) STRUCT_FOR_ID(host) @@ -576,6 +578,7 @@ struct _Py_global_strings { STRUCT_FOR_ID(insert_comments) STRUCT_FOR_ID(insert_pis) STRUCT_FOR_ID(instructions) + STRUCT_FOR_ID(int) STRUCT_FOR_ID(intern) STRUCT_FOR_ID(intersection) STRUCT_FOR_ID(interval) @@ -583,6 +586,7 @@ struct _Py_global_strings { STRUCT_FOR_ID(is_compress) STRUCT_FOR_ID(is_raw) STRUCT_FOR_ID(is_running) + STRUCT_FOR_ID(is_safe) STRUCT_FOR_ID(is_struct) STRUCT_FOR_ID(isatty) STRUCT_FOR_ID(isinstance) diff --git a/Include/internal/pycore_runtime_init_generated.h b/Include/internal/pycore_runtime_init_generated.h index 17ef3000b512d54..ba381a73d273443 100644 --- a/Include/internal/pycore_runtime_init_generated.h +++ b/Include/internal/pycore_runtime_init_generated.h @@ -1624,6 +1624,7 @@ extern "C" { INIT_ID(byte_size), \ INIT_ID(byteorder), \ INIT_ID(bytes), \ + INIT_ID(bytes_le), \ INIT_ID(bytes_per_sep), \ INIT_ID(c_call), \ INIT_ID(c_exception), \ @@ -1818,6 +1819,7 @@ extern "C" { INIT_ID(hash_name), \ INIT_ID(header), \ INIT_ID(headers), \ + INIT_ID(hex), \ INIT_ID(hi), \ INIT_ID(hook), \ INIT_ID(host), \ @@ -1851,6 +1853,7 @@ extern "C" { INIT_ID(insert_comments), \ INIT_ID(insert_pis), \ INIT_ID(instructions), \ + INIT_ID(int), \ INIT_ID(intern), \ INIT_ID(intersection), \ INIT_ID(interval), \ @@ -1858,6 +1861,7 @@ extern "C" { INIT_ID(is_compress), \ INIT_ID(is_raw), \ INIT_ID(is_running), \ + INIT_ID(is_safe), \ INIT_ID(is_struct), \ INIT_ID(isatty), \ INIT_ID(isinstance), \ diff --git a/Include/internal/pycore_unicodeobject_generated.h b/Include/internal/pycore_unicodeobject_generated.h index 5fff1df282cd4af..4ca1aac55e93771 100644 --- a/Include/internal/pycore_unicodeobject_generated.h +++ b/Include/internal/pycore_unicodeobject_generated.h @@ -301,6 +301,7 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { offsetof(struct _Py_global_strings, identifiers._py_byte_size._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_byteorder._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_bytes._ascii.ob_base), + offsetof(struct _Py_global_strings, identifiers._py_bytes_le._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_bytes_per_sep._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_c_call._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_c_exception._ascii.ob_base), @@ -495,6 +496,7 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { offsetof(struct _Py_global_strings, identifiers._py_hash_name._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_header._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_headers._ascii.ob_base), + offsetof(struct _Py_global_strings, identifiers._py_hex._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_hi._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_hook._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_host._ascii.ob_base), @@ -528,6 +530,7 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { offsetof(struct _Py_global_strings, identifiers._py_insert_comments._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_insert_pis._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_instructions._ascii.ob_base), + offsetof(struct _Py_global_strings, identifiers._py_int._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_intern._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_intersection._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_interval._ascii.ob_base), @@ -535,6 +538,7 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { offsetof(struct _Py_global_strings, identifiers._py_is_compress._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_is_raw._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_is_running._ascii.ob_base), + offsetof(struct _Py_global_strings, identifiers._py_is_safe._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_is_struct._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_isatty._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_isinstance._ascii.ob_base), diff --git a/Modules/_uuidmodule.c b/Modules/_uuidmodule.c index e607ebf066da861..808956afb2095ca 100644 --- a/Modules/_uuidmodule.c +++ b/Modules/_uuidmodule.c @@ -21,6 +21,7 @@ #include "pycore_long.h" // _PyLong_FromByteArray, _PyLong_AsByteArray #include "pycore_pylifecycle.h" // _PyOS_URandom() #include "pycore_time.h" // PyTime_Time +#include "pycore_runtime_init.h" // _Py_ID() #if defined(HAVE_UUID_H) // AIX, FreeBSD, libuuid with pkgconf @@ -1453,14 +1454,14 @@ _uuid_UUID___getstate___impl(uuidobject *self) Py_DECREF(dict); return NULL; } - if (PyDict_SetItemString(dict, "int", int_value) < 0) { + if (PyDict_SetItem(dict, &_Py_ID(int), int_value) < 0) { Py_DECREF(int_value); Py_DECREF(dict); return NULL; } Py_DECREF(int_value); - if (PyDict_SetItemString(dict, "is_safe", self->is_safe) < 0) { + if (PyDict_SetItem(dict, &_Py_ID(is_safe), self->is_safe) < 0) { Py_DECREF(dict); return NULL; } @@ -1491,7 +1492,7 @@ _uuid_UUID___setstate___impl(uuidobject *self, PyObject *state) } // Get and set the 'int' value - PyObject *int_value = PyDict_GetItemString(state, "int"); + PyObject *int_value = PyDict_GetItem(state, &_Py_ID(int)); if (int_value == NULL) { PyErr_SetString(PyExc_ValueError, "state must have 'int' key"); return NULL; @@ -1502,7 +1503,7 @@ _uuid_UUID___setstate___impl(uuidobject *self, PyObject *state) } // Get and set 'is_safe' if present - PyObject *is_safe = PyDict_GetItemString(state, "is_safe"); + PyObject *is_safe = PyDict_GetItem(state, &_Py_ID(is_safe)); if (is_safe != NULL) { // is_safe is the integer value, we need to call SafeUUID(value) PyObject *safe_uuid_member = PyObject_CallOneArg(module_state->safe_uuid, is_safe); From 623d1ab4a8a79660a19648e5703dc77ddabbbf3e Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Sun, 21 Sep 2025 19:51:37 +0100 Subject: [PATCH 09/38] Ensure total compatibility of C/Python implementations of uuid4 / uuid7 A side effect of some compatility fixes is the new code, with which the new C uuid7() is now 35x faster that pure Python (used to be 30x). --- Lib/test/test_uuid.py | 67 ++++++++++++ Lib/uuid.py | 37 ++++++- Modules/_uuidmodule.c | 183 +++++++++++++++++++++++++++------ Modules/clinic/_uuidmodule.c.h | 64 +++++++++++- 4 files changed, 312 insertions(+), 39 deletions(-) diff --git a/Lib/test/test_uuid.py b/Lib/test/test_uuid.py index e27aeff3efcb132..40187c3958ec2c6 100755 --- a/Lib/test/test_uuid.py +++ b/Lib/test/test_uuid.py @@ -1580,6 +1580,24 @@ def test_windll_getnode(self): self.check_node(node) +class UuidHooks: + + def __init__(self, *, start_at=0, inc_by=0, seed=0): + self._time = start_at + self._inc_by = inc_by + self._rnd = random.Random(seed) + + def random_func (self, size) : + ret = b'' + for _ in range(size) : + ret += self._rnd.getrandbits(8).to_bytes(1, 'big') + return ret + + def time_func(self): + self._time += self._inc_by + return self._time + + @unittest.skipUnless(c_uuid, "requires the C _uuid module") class TestCImplementationCompat(unittest.TestCase): def test_compatibility(self): @@ -1651,6 +1669,55 @@ def full_test(p, u): self.assertEqual(len(all_ps), len(all_us)) self.assertEqual(len(all_ps), len(uuids)) + def _install_hooks(self, uuid_mod, *, start_at=0, inc_by=0, seed=0): + py_hooks = UuidHooks(start_at=start_at, inc_by=inc_by, seed=seed) + uuid_mod._install_py_hooks( + random_func=py_hooks.random_func, + time_func=py_hooks.time_func + ) + + c_hooks = UuidHooks(start_at=start_at, inc_by=inc_by, seed=seed) + uuid_mod._install_c_hooks( + random_func=c_hooks.random_func, + time_func=c_hooks.time_func + ) + + def _reset_hooks(self, uuid_mod): + uuid_mod._install_c_hooks(random_func=None, time_func=None) + uuid_mod._install_py_hooks(random_func=None, time_func=None) + + def test_exact_same_algo_uuid4(self): + import uuid + + self._install_hooks(uuid) + try: + for seq_number in range(100): + with self.subTest(seq_number=seq_number): + self.assertEqual( + uuid._py_uuid4().hex, + uuid._c_uuid4().hex, + ) + finally: + self._reset_hooks(uuid) + + def test_exact_same_algo_uuid7(self): + import uuid + + try: + for start_at, inc_by in [ + (0, 0), (0, 10_000_000), (10_000_000 + 142, 1131827398127397) + ]: + self._install_hooks(uuid, start_at=start_at, inc_by=inc_by) + for seq_number in range(100): + with self.subTest( + seq_number=seq_number, start_at=start_at, inc_by=inc_by, + ): + self.assertEqual( + uuid._py_uuid7().hex, + uuid._c_uuid7().hex, + ) + finally: + self._reset_hooks(uuid) if __name__ == '__main__': diff --git a/Lib/uuid.py b/Lib/uuid.py index 485799dfa245e15..cd1feb2e9e3aa6b 100644 --- a/Lib/uuid.py +++ b/Lib/uuid.py @@ -111,6 +111,33 @@ class SafeUUID: _RFC_4122_VERSION_7_FLAGS = ((7 << 76) | (0x8000 << 48)) _RFC_4122_VERSION_8_FLAGS = ((8 << 76) | (0x8000 << 48)) +_random_hook = None +_time_hook = None + + +def _gen_random(size): + if _random_hook is None: + return os.urandom(size) + else: + return _random_hook(size) + + +def _gen_time(): + if _time_hook is None: + return time.time_ns() + else: + return _time_hook() + + +def _install_py_hooks(*, random_func, time_func): + global _random_hook + global _time_hook + global _last_timestamp_v7 + _random_hook = random_func + _time_hook = time_func + # Reset _last_timestamp_v7 for repeatability of tests + _last_timestamp_v7 = None + # Import optional C extension at toplevel, to help disabling it when testing try: @@ -783,7 +810,7 @@ def uuid3(namespace, name): def uuid4(): """Generate a random UUID.""" - int_uuid_4 = int.from_bytes(os.urandom(16)) + int_uuid_4 = int.from_bytes(_gen_random(16)) int_uuid_4 &= _RFC_4122_CLEARFLAGS_MASK int_uuid_4 |= _RFC_4122_VERSION_4_FLAGS return UUID._from_int(int_uuid_4) @@ -843,7 +870,8 @@ def uuid6(node=None, clock_seq=None): _last_counter_v7 = 0 # 42-bit counter def _uuid7_get_counter_and_tail(): - rand = int.from_bytes(os.urandom(10)) + rand = int.from_bytes(_gen_random(10), 'big') + # 42-bit counter with MSB set to 0 counter = (rand >> 32) & 0x1ff_ffff_ffff # 32-bit random data @@ -872,7 +900,7 @@ def uuid7(): global _last_timestamp_v7 global _last_counter_v7 - nanoseconds = time.time_ns() + nanoseconds = _gen_time() timestamp_ms = nanoseconds // 1_000_000 if _last_timestamp_v7 is None or timestamp_ms > _last_timestamp_v7: @@ -888,7 +916,7 @@ def uuid7(): counter, tail = _uuid7_get_counter_and_tail() else: # 32-bit random data - tail = int.from_bytes(os.urandom(4)) + tail = int.from_bytes(_gen_random(4)) unix_ts_ms = timestamp_ms & 0xffff_ffff_ffff counter_msbs = counter >> 30 @@ -946,6 +974,7 @@ def uuid8(a=None, b=None, c=None): _py_UUID = UUID try: from _uuid import UUID, uuid4, uuid7 + from _uuid import _install_c_hooks except ImportError: _c_UUID = None _c_uuid4 = None diff --git a/Modules/_uuidmodule.c b/Modules/_uuidmodule.c index 808956afb2095ca..4c044304e618fc8 100644 --- a/Modules/_uuidmodule.c +++ b/Modules/_uuidmodule.c @@ -179,9 +179,13 @@ typedef struct { PyObject *reserved_microsoft; PyObject *reserved_future; - // UUID v7 state - uint64_t last_timestamp_v7; - uint64_t last_counter_v7; + PyObject *random_func; + PyObject *random_size_int; + PyObject *time_func; + + // A freelist for uuid objects -- 15-20% performance boost. + uuidobject *freelist; + uint64_t freelist_size; // We overfetch entropy to speed up successive uuid generations; // this enables 10x peformance boost. @@ -189,9 +193,10 @@ typedef struct { uint64_t random_idx; uint64_t random_last_pid; - // A freelist for uuid objects -- 15-20% performance boost. - uuidobject *freelist; - uint64_t freelist_size; + // UUID v7 state + uint64_t last_timestamp_v7; + uint64_t last_counter_v7; + uint8_t last_timestamp_v7_init; } uuid_state; #include "clinic/_uuidmodule.c.h" @@ -235,6 +240,27 @@ get_uuid_state_by_cls(PyTypeObject *cls) // Forward declaration static PyObject *uuid_from_bytes_array(PyTypeObject *type, uint8_t bytes[16]); +static int +gen_time(uuid_state *state, PyTime_t* time) +{ + if (state->time_func == NULL) { + return PyTime_Time(time); + } + + PyObject *ret = PyObject_CallNoArgs(state->time_func); + if (ret == NULL) { + return -1; + } + + if (!PyLong_CheckExact(ret)) { + PyErr_SetString(PyExc_ValueError, "random_time must return int"); + } + + int res = PyLong_AsInt64(ret, time); + Py_DECREF(ret); + return res; +} + static int gen_random(uuid_state *state, uint8_t *bytes, Py_ssize_t size) { @@ -254,19 +280,62 @@ gen_random(uuid_state *state, uint8_t *bytes, Py_ssize_t size) state->random_idx = RANDOM_BUF_SIZE; } - if (state->random_idx + size <= RANDOM_BUF_SIZE) { + if (state->random_idx + size < RANDOM_BUF_SIZE) { memcpy(bytes, state->random_buf + state->random_idx, size); state->random_idx += size; } else { - // Pure Python implementation uses os.urandom() which - // wraps _PyOS_URandom - if (_PyOS_URandom(state->random_buf, RANDOM_BUF_SIZE) < 0) { - return -1; + if (state->random_idx < RANDOM_BUF_SIZE) { + // We exhaustively consume cached entropy. We do that + // because we have tests that compare Python and C + // implementations and it's important that they see incoming + // entropy as a continuous stream. + // The overhead here must be negligible, but we want the same + // code to be run in production and in tests. + Py_ssize_t partial = RANDOM_BUF_SIZE - state->random_idx; + memcpy(bytes, state->random_buf + state->random_idx, partial); + bytes += partial; + size -= partial; + } + + if (state->random_func != NULL) { + PyObject *buf = PyObject_CallOneArg( + state->random_func, state->random_size_int); + if (buf == NULL) { + return -1; + } + + if (!PyBytes_CheckExact(buf)) { + PyErr_SetString(PyExc_ValueError, "random_func must return bytes"); + Py_DECREF(buf); + return -1; + } + + if (PyBytes_Size(buf) != (Py_ssize_t)RANDOM_BUF_SIZE) { + PyErr_Format( + PyExc_ValueError, + "random_func must return bytes of length %zd exactly", + (Py_ssize_t)RANDOM_BUF_SIZE + ); + Py_DECREF(buf); + return -1; + } + + memcpy(state->random_buf, PyBytes_AsString(buf), RANDOM_BUF_SIZE); + Py_DECREF(buf); + } + else { + // Pure Python implementation uses os.urandom() which + // wraps _PyOS_URandom + if (_PyOS_URandom(state->random_buf, RANDOM_BUF_SIZE) < 0) { + return -1; + } } + memcpy(bytes, state->random_buf, size); state->random_idx = size; } + return 0; } @@ -296,25 +365,21 @@ _uuid_uuid4_impl(PyObject *module) } static inline int -uuid7_get_counter_and_tail(uuid_state *state, uint64_t *counter, uint32_t *tail) +uuid7_get_counter_and_tail(uuid_state *state, uint64_t *counter, uint8_t *tail) { uint8_t rand_bytes[10]; if (gen_random(state, rand_bytes, 10) < 0) { return -1; } - uint16_t high = ((uint16_t)rand_bytes[0] << 8) | rand_bytes[1]; - uint64_t low = ((uint64_t)rand_bytes[2] << 56) | - ((uint64_t)rand_bytes[3] << 48) | - ((uint64_t)rand_bytes[4] << 40) | - ((uint64_t)rand_bytes[5] << 32) | - ((uint64_t)rand_bytes[6] << 24) | - ((uint64_t)rand_bytes[7] << 16) | - ((uint64_t)rand_bytes[8] << 8) | - ((uint64_t)rand_bytes[9]); + *counter = (((uint64_t)rand_bytes[0] & 0x01) << 40) | + ((uint64_t)rand_bytes[1] << 32) | + ((uint64_t)rand_bytes[2] << 24) | + ((uint64_t)rand_bytes[3] << 16) | + ((uint64_t)rand_bytes[4] << 8) | + ((uint64_t)rand_bytes[5]); - *counter = (((uint64_t)(high & 0x1FF) << 32) | (low >> 32)) & 0x1FFFFFFFFFF; - *tail = (uint32_t)low; + memcpy(tail, rand_bytes + 6, 4); return 0; } @@ -338,16 +403,15 @@ _uuid_uuid7_impl(PyObject *module) uuid_state *state = get_uuid_state(module); uint8_t bytes[16]; uint64_t timestamp_ms, counter; - uint32_t tail; PyTime_t pytime; - if (PyTime_Time(&pytime) < 0) { + if (gen_time(state, &pytime) < 0) { return NULL; } timestamp_ms = (uint64_t)(pytime / 1000000); - if (state->last_timestamp_v7 == 0 || timestamp_ms > state->last_timestamp_v7) { - if (uuid7_get_counter_and_tail(state, &counter, &tail) < 0) { + if (!state->last_timestamp_v7_init || timestamp_ms > state->last_timestamp_v7) { + if (uuid7_get_counter_and_tail(state, &counter, bytes + 12) < 0) { return NULL; } } else { @@ -359,13 +423,13 @@ _uuid_uuid7_impl(PyObject *module) if (counter > 0x3FFFFFFFFFF) { // advance the 48-bit timestamp timestamp_ms += 1; - if (uuid7_get_counter_and_tail(state, &counter, &tail) < 0) { + if (uuid7_get_counter_and_tail(state, &counter, bytes + 12) < 0) { return NULL; } } else { // This is the common fast path, we only need 4 bytes of entropy // 32-bit random data - if (gen_random(state, (uint8_t *)&tail, 4) < 0) { + if (gen_random(state, bytes + 12, 4) < 0) { return NULL; } } @@ -391,17 +455,53 @@ _uuid_uuid7_impl(PyObject *module) bytes[10] = counter_lo >> 8; bytes[11] = counter_lo; - bytes[12] = tail >> 24; - bytes[13] = tail >> 16; - bytes[14] = tail >> 8; - bytes[15] = tail; - + state->last_timestamp_v7_init = 1; state->last_timestamp_v7 = timestamp_ms; state->last_counter_v7 = counter; return uuid_from_bytes_array(state->UuidType, bytes); } +/*[clinic input] +@critical_section +_uuid._install_c_hooks + + * + random_func: object + time_func: object + +[clinic start generated code]*/ + +static PyObject * +_uuid__install_c_hooks_impl(PyObject *module, PyObject *random_func, + PyObject *time_func) +/*[clinic end generated code: output=884aa6e91b2ea832 input=6c5017297067e2ea]*/ +{ + uuid_state *state = get_uuid_state(module); + + // Reset bufferred entropy -- tests need to always start fresh + // when the random function is changed. Reset last_timestamp_v7 -- + // important for repeatable tests + state->random_idx = RANDOM_BUF_SIZE; + state->last_timestamp_v7_init = 0; + + if (random_func == Py_None) { + Py_CLEAR(state->random_func); + } else { + Py_INCREF(random_func); + Py_XSETREF(state->random_func, random_func); + } + + if (time_func == Py_None) { + Py_CLEAR(state->time_func); + } else { + Py_INCREF(time_func); + Py_XSETREF(state->time_func, time_func); + } + + Py_RETURN_NONE; +} + /*[clinic input] _uuid.UUID.__init__ @@ -1585,6 +1685,9 @@ module_traverse(PyObject *mod, visitproc visit, void *arg) Py_VISIT(state->rfc_4122); Py_VISIT(state->reserved_microsoft); Py_VISIT(state->reserved_future); + Py_VISIT(state->random_func); + Py_VISIT(state->time_func); + Py_VISIT(state->random_size_int); return 0; } @@ -1603,6 +1706,9 @@ module_clear(PyObject *mod) Py_CLEAR(state->rfc_4122); Py_CLEAR(state->reserved_microsoft); Py_CLEAR(state->reserved_future); + Py_CLEAR(state->random_func); + Py_CLEAR(state->time_func); + Py_CLEAR(state->random_size_int); if (state->freelist != NULL) { while (state->freelist != NULL) { @@ -1717,9 +1823,17 @@ uuid_exec(PyObject *module) } state->last_timestamp_v7 = 0; + state->last_timestamp_v7_init = 0; state->last_counter_v7 = 0; state->random_last_pid = uuid_getpid(); + state->time_func = NULL; + state->random_func = NULL; + state->random_size_int = PyLong_FromSize_t((Py_ssize_t)RANDOM_BUF_SIZE); + if (state->random_size_int == NULL) { + goto fail; + } + state->freelist = NULL; state->freelist_size = 0; @@ -1736,6 +1850,7 @@ uuid_exec(PyObject *module) static PyMethodDef uuid_methods[] = { _UUID_UUID4_METHODDEF _UUID_UUID7_METHODDEF + _UUID__INSTALL_C_HOOKS_METHODDEF #if defined(HAVE_UUID_UUID_H) || defined(HAVE_UUID_H) {"generate_time_safe", py_uuid_generate_time_safe, METH_NOARGS, NULL}, #endif diff --git a/Modules/clinic/_uuidmodule.c.h b/Modules/clinic/_uuidmodule.c.h index 3d9c374d83087d5..f8fffa9b41b7b7a 100644 --- a/Modules/clinic/_uuidmodule.c.h +++ b/Modules/clinic/_uuidmodule.c.h @@ -59,6 +59,68 @@ _uuid_uuid7(PyObject *module, PyObject *Py_UNUSED(ignored)) return return_value; } +PyDoc_STRVAR(_uuid__install_c_hooks__doc__, +"_install_c_hooks($module, /, *, random_func, time_func)\n" +"--\n" +"\n"); + +#define _UUID__INSTALL_C_HOOKS_METHODDEF \ + {"_install_c_hooks", _PyCFunction_CAST(_uuid__install_c_hooks), METH_FASTCALL|METH_KEYWORDS, _uuid__install_c_hooks__doc__}, + +static PyObject * +_uuid__install_c_hooks_impl(PyObject *module, PyObject *random_func, + PyObject *time_func); + +static PyObject * +_uuid__install_c_hooks(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) +{ + PyObject *return_value = NULL; + #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) + + #define NUM_KEYWORDS 2 + static struct { + PyGC_Head _this_is_not_used; + PyObject_VAR_HEAD + Py_hash_t ob_hash; + PyObject *ob_item[NUM_KEYWORDS]; + } _kwtuple = { + .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) + .ob_hash = -1, + .ob_item = { &_Py_ID(random_func), &_Py_ID(time_func), }, + }; + #undef NUM_KEYWORDS + #define KWTUPLE (&_kwtuple.ob_base.ob_base) + + #else // !Py_BUILD_CORE + # define KWTUPLE NULL + #endif // !Py_BUILD_CORE + + static const char * const _keywords[] = {"random_func", "time_func", NULL}; + static _PyArg_Parser _parser = { + .keywords = _keywords, + .fname = "_install_c_hooks", + .kwtuple = KWTUPLE, + }; + #undef KWTUPLE + PyObject *argsbuf[2]; + PyObject *random_func; + PyObject *time_func; + + args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, + /*minpos*/ 0, /*maxpos*/ 0, /*minkw*/ 2, /*varpos*/ 0, argsbuf); + if (!args) { + goto exit; + } + random_func = args[0]; + time_func = args[1]; + Py_BEGIN_CRITICAL_SECTION(module); + return_value = _uuid__install_c_hooks_impl(module, random_func, time_func); + Py_END_CRITICAL_SECTION(); + +exit: + return return_value; +} + PyDoc_STRVAR(_uuid_UUID___init____doc__, "UUID(hex=, bytes=None, bytes_le=None,\n" " fields=, int=,\n" @@ -252,4 +314,4 @@ _uuid_UUID___setstate__(PyObject *self, PyObject *state) return return_value; } -/*[clinic end generated code: output=ec50cafa0e028d2b input=a9049054013a1b77]*/ +/*[clinic end generated code: output=095610812af4b3bd input=a9049054013a1b77]*/ From 32a8911e440594a002ee0259465088d77b3e9b15 Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Sun, 21 Sep 2025 20:48:48 +0100 Subject: [PATCH 10/38] Stop importing unused SafeUUID members --- Modules/_uuidmodule.c | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/Modules/_uuidmodule.c b/Modules/_uuidmodule.c index 4c044304e618fc8..88a42c538d59059 100644 --- a/Modules/_uuidmodule.c +++ b/Modules/_uuidmodule.c @@ -168,8 +168,6 @@ typedef struct { PyTypeObject *UuidType; PyObject *safe_uuid; - PyObject *safe_uuid_safe; - PyObject *safe_uuid_unsafe; PyObject *safe_uuid_unknown; PyObject *uint128_max; @@ -1677,8 +1675,6 @@ module_traverse(PyObject *mod, visitproc visit, void *arg) uuid_state *state = get_uuid_state(mod); Py_VISIT(state->UuidType); Py_VISIT(state->safe_uuid); - Py_VISIT(state->safe_uuid_safe); - Py_VISIT(state->safe_uuid_unsafe); Py_VISIT(state->safe_uuid_unknown); Py_VISIT(state->uint128_max); Py_VISIT(state->reserved_ncs); @@ -1698,8 +1694,6 @@ module_clear(PyObject *mod) Py_CLEAR(state->UuidType); Py_CLEAR(state->safe_uuid); - Py_CLEAR(state->safe_uuid_safe); - Py_CLEAR(state->safe_uuid_unsafe); Py_CLEAR(state->safe_uuid_unknown); Py_CLEAR(state->uint128_max); Py_CLEAR(state->reserved_ncs); @@ -1784,14 +1778,6 @@ uuid_exec(PyObject *module) if (safe_uuid == NULL) { goto fail; } - state->safe_uuid_safe = PyObject_GetAttrString(safe_uuid, "safe"); - if (state->safe_uuid_safe == NULL) { - goto fail; - } - state->safe_uuid_unsafe = PyObject_GetAttrString(safe_uuid, "unsafe"); - if (state->safe_uuid_unsafe == NULL) { - goto fail; - } state->safe_uuid_unknown = PyObject_GetAttrString(safe_uuid, "unknown"); if (state->safe_uuid_unknown == NULL) { goto fail; From 7880664e2f0290e140e97412f6b10809290124ad Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Sun, 21 Sep 2025 20:58:46 +0100 Subject: [PATCH 11/38] Drop SafeUUID.unknown --- Modules/_uuidmodule.c | 76 +++++++++++++++++++++++++------------------ 1 file changed, 45 insertions(+), 31 deletions(-) diff --git a/Modules/_uuidmodule.c b/Modules/_uuidmodule.c index 88a42c538d59059..b21601a044cda84 100644 --- a/Modules/_uuidmodule.c +++ b/Modules/_uuidmodule.c @@ -168,7 +168,6 @@ typedef struct { PyTypeObject *UuidType; PyObject *safe_uuid; - PyObject *safe_uuid_unknown; PyObject *uint128_max; @@ -971,12 +970,7 @@ make_uuid(PyTypeObject *type) } } - // During module initialization, safe_uuid_unknown might not be set yet - if (state->safe_uuid_unknown != NULL) { - self->is_safe = Py_NewRef(state->safe_uuid_unknown); - } else { - self->is_safe = Py_NewRef(Py_None); - } + self->is_safe = NULL; self->weakreflist = NULL; self->cached_hash = -1; @@ -1036,8 +1030,9 @@ static PyObject * Uuid_get_is_safe(PyObject *o, void *closure) { uuidobject *self = (uuidobject *)o; + uuid_state *state = get_uuid_state_by_cls(Py_TYPE(self)); if (self->is_safe == NULL) { - Py_RETURN_NONE; + return PyObject_GetAttrString(state->safe_uuid, "unknown"); } return Py_NewRef(self->is_safe); } @@ -1582,8 +1577,6 @@ static PyObject * _uuid_UUID___setstate___impl(uuidobject *self, PyObject *state) /*[clinic end generated code: output=cdf6bd4a2a680b3f input=b1ec0744788a73a0]*/ { - uuid_state *module_state = get_uuid_state_by_cls(Py_TYPE(self)); - if (!PyDict_Check(state)) { PyErr_SetString(PyExc_TypeError, "state must be a dictionary"); return NULL; @@ -1600,25 +1593,52 @@ _uuid_UUID___setstate___impl(uuidobject *self, PyObject *state) return NULL; } - // Get and set 'is_safe' if present - PyObject *is_safe = PyDict_GetItem(state, &_Py_ID(is_safe)); - if (is_safe != NULL) { - // is_safe is the integer value, we need to call SafeUUID(value) - PyObject *safe_uuid_member = PyObject_CallOneArg(module_state->safe_uuid, is_safe); - if (safe_uuid_member == NULL) { - return NULL; - } - Py_XDECREF(self->is_safe); - self->is_safe = safe_uuid_member; - } else { - // No is_safe in state, set to SafeUUID.unknown - Py_XDECREF(self->is_safe); - self->is_safe = Py_NewRef(module_state->safe_uuid_unknown); + Py_CLEAR(self->is_safe); + if (PyDict_GetItemRef(state, &_Py_ID(is_safe), &self->is_safe) < 0) { + return NULL; } Py_RETURN_NONE; } +static PyObject * +compute_uuid_max(void) +{ + // Compute `(1 << 128) - 1` + + PyObject *one = NULL; + PyObject *shift = NULL; + PyObject *shifted = NULL; + + one = PyLong_FromLong(1); + if (one == NULL) { + goto err; + } + + shift = PyLong_FromLong(128); + if (shift == NULL) { + goto err; + } + + shifted = PyNumber_Lshift(one, shift); + if (shifted == NULL) { + goto err; + } + Py_DECREF(shift); + + PyObject *result = PyNumber_Subtract(shifted, one); + Py_DECREF(shifted); + Py_DECREF(one); + + return result; + +err: + Py_XDECREF(one); + Py_XDECREF(shift); + Py_XDECREF(shifted); + return NULL; +} + static PyMethodDef Uuid_methods[] = { _UUID_UUID__FROM_INT_METHODDEF _UUID_UUID___GETSTATE___METHODDEF @@ -1675,7 +1695,6 @@ module_traverse(PyObject *mod, visitproc visit, void *arg) uuid_state *state = get_uuid_state(mod); Py_VISIT(state->UuidType); Py_VISIT(state->safe_uuid); - Py_VISIT(state->safe_uuid_unknown); Py_VISIT(state->uint128_max); Py_VISIT(state->reserved_ncs); Py_VISIT(state->rfc_4122); @@ -1694,7 +1713,6 @@ module_clear(PyObject *mod) Py_CLEAR(state->UuidType); Py_CLEAR(state->safe_uuid); - Py_CLEAR(state->safe_uuid_unknown); Py_CLEAR(state->uint128_max); Py_CLEAR(state->reserved_ncs); Py_CLEAR(state->rfc_4122); @@ -1778,12 +1796,8 @@ uuid_exec(PyObject *module) if (safe_uuid == NULL) { goto fail; } - state->safe_uuid_unknown = PyObject_GetAttrString(safe_uuid, "unknown"); - if (state->safe_uuid_unknown == NULL) { - goto fail; - } - state->uint128_max = PyObject_GetAttrString(uuid_mod, "_UINT_128_MAX"); + state->uint128_max = compute_uuid_max(); if (state->uint128_max == NULL) { goto fail; } From f80ef7528b2e6aaca8dc9101da1dd5cde65a7f1e Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Sun, 21 Sep 2025 21:06:07 +0100 Subject: [PATCH 12/38] Drop more things that can be imported dynamically --- Modules/_uuidmodule.c | 54 ++++++++++++++----------------------------- 1 file changed, 17 insertions(+), 37 deletions(-) diff --git a/Modules/_uuidmodule.c b/Modules/_uuidmodule.c index b21601a044cda84..ca3ca658735c17b 100644 --- a/Modules/_uuidmodule.c +++ b/Modules/_uuidmodule.c @@ -171,11 +171,6 @@ typedef struct { PyObject *uint128_max; - PyObject *reserved_ncs; - PyObject *rfc_4122; - PyObject *reserved_microsoft; - PyObject *reserved_future; - PyObject *random_func; PyObject *random_size_int; PyObject *time_func; @@ -1048,6 +1043,19 @@ Uuid_get_hex(PyObject *o, void *closure) return PyUnicode_FromStringAndSize(hex, 32); } +static PyObject * +get_variant_marker(uuid_state *state, const char *name) +{ + PyObject *mod = PyImport_ImportModule("uuid"); + if (mod == NULL) { + return NULL; + } + + PyObject *ret = PyObject_GetAttrString(mod, name); + Py_DECREF(mod); + return ret; +} + static PyObject * Uuid_get_variant(PyObject *o, void *closure) { @@ -1059,23 +1067,23 @@ Uuid_get_variant(PyObject *o, void *closure) // xxx - three high bits of variant_byte are unknown if (!(variant_byte & 0x80)) { // & 0b1000_0000 // 0xx - RESERVED_NCS - return Py_NewRef(state->reserved_ncs); + return get_variant_marker(state, "RESERVED_NCS"); } // 1xx -- we know that high bit must be 1 if (!(variant_byte & 0x40)) { // & 0b0100_0000 // 10x - RFC_4122 - return Py_NewRef(state->rfc_4122); + return get_variant_marker(state, "RFC_4122"); } // 11x -- we know that two high bits are 1 if (!(variant_byte & 0x20)) { // & 0b0010_0000 // 110 - RESERVED_MICROSOFT - return Py_NewRef(state->reserved_microsoft); + return get_variant_marker(state, "RESERVED_MICROSOFT"); } // 111 -- we know that all three high bits are 1 - RESERVED_FUTURE - return Py_NewRef(state->reserved_future); + return get_variant_marker(state, "RESERVED_FUTURE"); } static int @@ -1696,10 +1704,6 @@ module_traverse(PyObject *mod, visitproc visit, void *arg) Py_VISIT(state->UuidType); Py_VISIT(state->safe_uuid); Py_VISIT(state->uint128_max); - Py_VISIT(state->reserved_ncs); - Py_VISIT(state->rfc_4122); - Py_VISIT(state->reserved_microsoft); - Py_VISIT(state->reserved_future); Py_VISIT(state->random_func); Py_VISIT(state->time_func); Py_VISIT(state->random_size_int); @@ -1714,10 +1718,6 @@ module_clear(PyObject *mod) Py_CLEAR(state->UuidType); Py_CLEAR(state->safe_uuid); Py_CLEAR(state->uint128_max); - Py_CLEAR(state->reserved_ncs); - Py_CLEAR(state->rfc_4122); - Py_CLEAR(state->reserved_microsoft); - Py_CLEAR(state->reserved_future); Py_CLEAR(state->random_func); Py_CLEAR(state->time_func); Py_CLEAR(state->random_size_int); @@ -1802,26 +1802,6 @@ uuid_exec(PyObject *module) goto fail; } - state->reserved_ncs = PyObject_GetAttrString(uuid_mod, "RESERVED_NCS"); - if (state->reserved_ncs == NULL) { - goto fail; - } - - state->rfc_4122 = PyObject_GetAttrString(uuid_mod, "RFC_4122"); - if (state->rfc_4122 == NULL) { - goto fail; - } - - state->reserved_microsoft = PyObject_GetAttrString(uuid_mod, "RESERVED_MICROSOFT"); - if (state->reserved_microsoft == NULL) { - goto fail; - } - - state->reserved_future = PyObject_GetAttrString(uuid_mod, "RESERVED_FUTURE"); - if (state->reserved_future == NULL) { - goto fail; - } - state->last_timestamp_v7 = 0; state->last_timestamp_v7_init = 0; state->last_counter_v7 = 0; From 860e7f7c04b5f6d7528ccf0cfe127b585e0bd9be Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Sun, 21 Sep 2025 21:29:12 +0100 Subject: [PATCH 13/38] Remove circular import; import 'uuid' from '_uuid' lazily --- Modules/_uuidmodule.c | 97 ++++++++++++++++++++++--------------------- 1 file changed, 49 insertions(+), 48 deletions(-) diff --git a/Modules/_uuidmodule.c b/Modules/_uuidmodule.c index ca3ca658735c17b..92fa614bc38732f 100644 --- a/Modules/_uuidmodule.c +++ b/Modules/_uuidmodule.c @@ -203,6 +203,7 @@ static int from_hex(uuidobject *self, PyObject *hex); static int from_bytes_le(uuidobject *self, Py_buffer *bytes_le); static int from_int(uuidobject *self, PyObject *int_value, int validate); static int from_fields(uuidobject *self, PyObject *fields); +static PyObject * get_SafeUUID(uuid_state *state); static uint64_t uuid_getpid(void) { @@ -517,8 +518,6 @@ _uuid_UUID___init___impl(uuidobject *self, PyObject *hex, Py_buffer *bytes, /*[clinic end generated code: output=93a6881c8f79bf9b input=b9c79672fbd76a99]*/ { - uuid_state *state = get_uuid_state_by_cls(Py_TYPE(self)); - int passed = 0; if (hex != NULL) passed++; if (bytes->obj != NULL) passed++; @@ -587,13 +586,13 @@ _uuid_UUID___init___impl(uuidobject *self, PyObject *hex, Py_buffer *bytes, self->bytes[6] |= (version_num << 4); } + if (is_safe == Py_None) { + // Py_None is immortal; skip decref + is_safe = NULL; + } if (is_safe != NULL) { - // Validate by calling SafeUUID(is_safe) to ensure it's a valid enum member - PyObject *validated = PyObject_CallOneArg(state->safe_uuid, is_safe); - if (validated == NULL) { - return -1; - } - Py_XSETREF(self->is_safe, validated); + Py_INCREF(is_safe); + Py_XSETREF(self->is_safe, is_safe); } return 0; @@ -940,6 +939,33 @@ get_int(uuidobject *self) return _PyLong_FromByteArray((unsigned char *)self->bytes, 16, 0, 0); } +static PyObject * +get_uuidmod_attr(uuid_state *state, const char *name) +{ + PyObject *mod = PyImport_ImportModule("uuid"); + if (mod == NULL) { + return NULL; + } + + PyObject *ret = PyObject_GetAttrString(mod, name); + Py_DECREF(mod); + return ret; +} + +static PyObject * +get_SafeUUID(uuid_state *state) +{ + if (state->safe_uuid != NULL) { + return Py_NewRef(state->safe_uuid); + } + + state->safe_uuid = get_uuidmod_attr(state, "SafeUUID"); + if (state->safe_uuid == NULL) { + return NULL; + } + return Py_NewRef(state->safe_uuid); +} + static uuidobject * make_uuid(PyTypeObject *type) { @@ -1027,7 +1053,10 @@ Uuid_get_is_safe(PyObject *o, void *closure) uuidobject *self = (uuidobject *)o; uuid_state *state = get_uuid_state_by_cls(Py_TYPE(self)); if (self->is_safe == NULL) { - return PyObject_GetAttrString(state->safe_uuid, "unknown"); + PyObject *SafeUUID = get_SafeUUID(state); + PyObject *ret = PyObject_GetAttrString(SafeUUID, "unknown"); + Py_DECREF(SafeUUID); + return ret; } return Py_NewRef(self->is_safe); } @@ -1043,19 +1072,6 @@ Uuid_get_hex(PyObject *o, void *closure) return PyUnicode_FromStringAndSize(hex, 32); } -static PyObject * -get_variant_marker(uuid_state *state, const char *name) -{ - PyObject *mod = PyImport_ImportModule("uuid"); - if (mod == NULL) { - return NULL; - } - - PyObject *ret = PyObject_GetAttrString(mod, name); - Py_DECREF(mod); - return ret; -} - static PyObject * Uuid_get_variant(PyObject *o, void *closure) { @@ -1067,23 +1083,23 @@ Uuid_get_variant(PyObject *o, void *closure) // xxx - three high bits of variant_byte are unknown if (!(variant_byte & 0x80)) { // & 0b1000_0000 // 0xx - RESERVED_NCS - return get_variant_marker(state, "RESERVED_NCS"); + return get_uuidmod_attr(state, "RESERVED_NCS"); } // 1xx -- we know that high bit must be 1 if (!(variant_byte & 0x40)) { // & 0b0100_0000 // 10x - RFC_4122 - return get_variant_marker(state, "RFC_4122"); + return get_uuidmod_attr(state, "RFC_4122"); } // 11x -- we know that two high bits are 1 if (!(variant_byte & 0x20)) { // & 0b0010_0000 // 110 - RESERVED_MICROSOFT - return get_variant_marker(state, "RESERVED_MICROSOFT"); + return get_uuidmod_attr(state, "RESERVED_MICROSOFT"); } // 111 -- we know that all three high bits are 1 - RESERVED_FUTURE - return get_variant_marker(state, "RESERVED_FUTURE"); + return get_uuidmod_attr(state, "RESERVED_FUTURE"); } static int @@ -1746,13 +1762,11 @@ static int uuid_exec(PyObject *module) { uuid_state *state = get_uuid_state(module); - PyObject *uuid_mod = NULL; - PyObject *safe_uuid = NULL; #define ADD_INT(NAME, VALUE) \ do { \ if (PyModule_AddIntConstant(module, (NAME), (VALUE)) < 0) { \ - goto fail; \ + return -1; \ } \ } while (0) @@ -1782,24 +1796,17 @@ uuid_exec(PyObject *module) NULL ); if (state->UuidType == NULL) { - goto fail; + return -1; } if (PyModule_AddType(module, state->UuidType) < 0) { - goto fail; + return -1; } - uuid_mod = PyImport_ImportModule("uuid"); - if (uuid_mod == NULL) { - goto fail; - } - safe_uuid = state->safe_uuid = PyObject_GetAttrString(uuid_mod, "SafeUUID"); - if (safe_uuid == NULL) { - goto fail; - } + state->safe_uuid = NULL; state->uint128_max = compute_uuid_max(); if (state->uint128_max == NULL) { - goto fail; + return -1; } state->last_timestamp_v7 = 0; @@ -1811,20 +1818,14 @@ uuid_exec(PyObject *module) state->random_func = NULL; state->random_size_int = PyLong_FromSize_t((Py_ssize_t)RANDOM_BUF_SIZE); if (state->random_size_int == NULL) { - goto fail; + return -1; } + state->random_idx = RANDOM_BUF_SIZE; state->freelist = NULL; state->freelist_size = 0; - state->random_idx = RANDOM_BUF_SIZE; - - Py_CLEAR(uuid_mod); return 0; - -fail: - Py_CLEAR(uuid_mod); - return -1; } static PyMethodDef uuid_methods[] = { From faee029f44219f0c4548c50d6ea75d5386adf2c3 Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Sun, 21 Sep 2025 21:22:47 -0700 Subject: [PATCH 14/38] Regen files --- Include/internal/pycore_global_objects_fini_generated.h | 2 ++ Include/internal/pycore_global_strings.h | 2 ++ Include/internal/pycore_runtime_init_generated.h | 2 ++ Include/internal/pycore_unicodeobject_generated.h | 2 ++ 4 files changed, 8 insertions(+) diff --git a/Include/internal/pycore_global_objects_fini_generated.h b/Include/internal/pycore_global_objects_fini_generated.h index b07e5c9894fe8fe..cf5bcbb2941d3d4 100644 --- a/Include/internal/pycore_global_objects_fini_generated.h +++ b/Include/internal/pycore_global_objects_fini_generated.h @@ -748,6 +748,7 @@ _PyStaticObjects_CheckAll(PyInterpreterState *interp) { _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(query), "query", 5); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(queuetype), "queuetype", 9); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(quotetabs), "quotetabs", 9); + _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(random_func), "random_func", 11); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(raw), "raw", 3); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(read), "read", 4); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(read1), "read1", 5); @@ -862,6 +863,7 @@ _PyStaticObjects_CheckAll(PyInterpreterState *interp) { _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(threading), "threading", 9); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(throw), "throw", 5); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(time), "time", 4); + _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(time_func), "time_func", 9); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(timeout), "timeout", 7); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(timer), "timer", 5); _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(times), "times", 5); diff --git a/Include/internal/pycore_global_strings.h b/Include/internal/pycore_global_strings.h index fedde521c838484..bc0a149bc939736 100644 --- a/Include/internal/pycore_global_strings.h +++ b/Include/internal/pycore_global_strings.h @@ -760,6 +760,7 @@ struct _Py_global_strings { STRUCT_FOR_ID(query) STRUCT_FOR_ID(queuetype) STRUCT_FOR_ID(quotetabs) + STRUCT_FOR_ID(random_func) STRUCT_FOR_ID(raw) STRUCT_FOR_ID(read) STRUCT_FOR_ID(read1) @@ -874,6 +875,7 @@ struct _Py_global_strings { STRUCT_FOR_ID(threading) STRUCT_FOR_ID(throw) STRUCT_FOR_ID(time) + STRUCT_FOR_ID(time_func) STRUCT_FOR_ID(timeout) STRUCT_FOR_ID(timer) STRUCT_FOR_ID(times) diff --git a/Include/internal/pycore_runtime_init_generated.h b/Include/internal/pycore_runtime_init_generated.h index ba381a73d273443..630b5d786713354 100644 --- a/Include/internal/pycore_runtime_init_generated.h +++ b/Include/internal/pycore_runtime_init_generated.h @@ -2035,6 +2035,7 @@ extern "C" { INIT_ID(query), \ INIT_ID(queuetype), \ INIT_ID(quotetabs), \ + INIT_ID(random_func), \ INIT_ID(raw), \ INIT_ID(read), \ INIT_ID(read1), \ @@ -2149,6 +2150,7 @@ extern "C" { INIT_ID(threading), \ INIT_ID(throw), \ INIT_ID(time), \ + INIT_ID(time_func), \ INIT_ID(timeout), \ INIT_ID(timer), \ INIT_ID(times), \ diff --git a/Include/internal/pycore_unicodeobject_generated.h b/Include/internal/pycore_unicodeobject_generated.h index 4ca1aac55e93771..621a03016301ab4 100644 --- a/Include/internal/pycore_unicodeobject_generated.h +++ b/Include/internal/pycore_unicodeobject_generated.h @@ -712,6 +712,7 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { offsetof(struct _Py_global_strings, identifiers._py_query._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_queuetype._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_quotetabs._ascii.ob_base), + offsetof(struct _Py_global_strings, identifiers._py_random_func._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_raw._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_read._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_read1._ascii.ob_base), @@ -826,6 +827,7 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { offsetof(struct _Py_global_strings, identifiers._py_threading._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_throw._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_time._ascii.ob_base), + offsetof(struct _Py_global_strings, identifiers._py_time_func._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_timeout._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_timer._ascii.ob_base), offsetof(struct _Py_global_strings, identifiers._py_times._ascii.ob_base), From a0c30070b38ae23d2b7544e38712b89d1845e85d Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Sun, 21 Sep 2025 21:41:05 -0700 Subject: [PATCH 15/38] Get module state assuming there could be subclasses of UUID This fixes a segfault; regr test added. While there, make the freelist only work with our base UUID type and no subclasses of it. --- Lib/test/test_uuid.py | 11 +++++++++++ Modules/_uuidmodule.c | 19 +++++++++++++++---- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_uuid.py b/Lib/test/test_uuid.py index 40187c3958ec2c6..6f331235776d9e0 100755 --- a/Lib/test/test_uuid.py +++ b/Lib/test/test_uuid.py @@ -1719,6 +1719,17 @@ def test_exact_same_algo_uuid7(self): finally: self._reset_hooks(uuid) + def test_subclassing(self): + import uuid + + class U(uuid._c_UUID): + pass + + u = U(int=1) + u_str = str(u) + del u + self.assertEqual(u_str, '00000000-0000-0000-0000-000000000001') + if __name__ == '__main__': unittest.main() diff --git a/Modules/_uuidmodule.c b/Modules/_uuidmodule.c index 92fa614bc38732f..1e68f8739b05115 100644 --- a/Modules/_uuidmodule.c +++ b/Modules/_uuidmodule.c @@ -214,6 +214,8 @@ uuid_getpid(void) { #endif } +static struct PyModuleDef uuidmodule; + static inline uuid_state * get_uuid_state(PyObject *mod) { @@ -225,7 +227,9 @@ get_uuid_state(PyObject *mod) static inline uuid_state * get_uuid_state_by_cls(PyTypeObject *cls) { - uuid_state *state = (uuid_state *)PyType_GetModuleState(cls); + PyObject *module = PyType_GetModuleByDef(cls, &uuidmodule); + assert(module != NULL); + uuid_state *state = get_uuid_state(module); assert(state != NULL); return state; } @@ -1022,18 +1026,25 @@ Uuid_dealloc(PyObject *obj) } Py_CLEAR(uuid->is_safe); - int added_to_freelist = 0; + int skip_dealloc = 0; + if (type != state->UuidType) { + // We only apply the freelist optimization to the known C type, + // not any subclasses of it. + goto dealloc; + } + Py_BEGIN_CRITICAL_SECTION(type); if (state->freelist_size < MAX_FREE_LIST_SIZE) { uuidobject *head = state->freelist; state->freelist = uuid; uuid->weakreflist = (PyObject *)head; state->freelist_size++; - added_to_freelist = 1; + skip_dealloc = 1; } Py_END_CRITICAL_SECTION(); - if (!added_to_freelist) { +dealloc: + if (!skip_dealloc) { type->tp_free(uuid); // UUID is a heap allocated type so we have to decref the type ref Py_DECREF(type); From f2687b46fa7571c418cd7d747251875e39de599e Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Sun, 21 Sep 2025 21:50:15 -0700 Subject: [PATCH 16/38] More ergonomic int cast code --- Modules/_uuidmodule.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Modules/_uuidmodule.c b/Modules/_uuidmodule.c index 1e68f8739b05115..ad31c57ec72a9a9 100644 --- a/Modules/_uuidmodule.c +++ b/Modules/_uuidmodule.c @@ -840,10 +840,8 @@ extract_field( goto fail; } - int overflow; - uint64_t value = PyLong_AsLongLongAndOverflow(field, &overflow); - if (overflow || (value == (uint64_t)-1 && PyErr_Occurred())) { - PyErr_SetString(PyExc_ValueError, error_msg); + uint64_t value; + if (PyLong_AsUInt64(field, &value) < 0) { goto fail; } From a1f5fcc0f71cb87d51ebe85600fd602c29c667a7 Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Mon, 19 Oct 2026 15:32:10 -0700 Subject: [PATCH 17/38] gh-139122: Fix UUID subclass allocation and freelist type ownership --- Lib/test/test_uuid.py | 61 +++++++++++++++++++++++++++++++++++++++++++ Modules/_uuidmodule.c | 23 ++++++++++++---- 2 files changed, 79 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_uuid.py b/Lib/test/test_uuid.py index 6f331235776d9e0..0eff0ed1fc9d827 100755 --- a/Lib/test/test_uuid.py +++ b/Lib/test/test_uuid.py @@ -7,6 +7,7 @@ import pickle import random import sys +import textwrap import unittest import weakref from itertools import product @@ -1730,6 +1731,66 @@ class U(uuid._c_UUID): del u self.assertEqual(u_str, '00000000-0000-0000-0000-000000000001') + def test_subclass_allocation(self): + # Subclasses must bypass the base UUID freelist and get enough storage + # for their slots and dictionary, with GC tracking for reference cycles. + # Run in a subprocess because an incorrectly sized or reused object + # can corrupt memory before the assertions detect it. + assert_python_ok('-c', textwrap.dedent(''' + import gc + import uuid + import weakref + + class U(uuid.UUID): + __slots__ = ('extra', '__dict__') + + # Exercise both the constructor and the internal factory. + for factory in (lambda cls: cls(int=1), lambda cls: cls._from_int(1)): + # Leave a base UUID available in the freelist. + base = uuid.UUID(int=0) + del base + u = factory(U) + # A subclass must not receive a cached base UUID. + assert type(u) is U + assert u.int == 1 + # Allocation must include storage for subclass slots and a dict. + object.__setattr__(u, 'extra', u) + object.__setattr__(u, 'other', 42) + assert u.extra is u + assert u.other == 42 + # The self-reference requires GC tracking to collect the object. + ref = weakref.ref(u) + del u + gc.collect() + assert ref() is None + + # Subclass allocation must leave the base freelist usable. + base = uuid.UUID(int=2) + assert type(base) is uuid.UUID + assert base.int == 2 + ''')) + + def test_freelist_type_lifetime(self): + # Reused live instances must keep their type and module alive, while + # cached instances must allow both to be collected. + module = import_helper.import_fresh_module('_uuid') + module_ref = weakref.ref(module) + type_ref = weakref.ref(module.UUID) + # Leave a base UUID available in the freelist. + obj = module.UUID(int=0) + del obj + # A reused live instance must still keep its type and module alive. + obj = module.UUID(int=1) + del module + support.gc_collect() + self.assertIsNotNone(type_ref()) + self.assertIsNotNone(module_ref()) + self.assertEqual(obj.int, 1) + del obj + support.gc_collect() + self.assertIsNone(type_ref()) + self.assertIsNone(module_ref()) + if __name__ == '__main__': unittest.main() diff --git a/Modules/_uuidmodule.c b/Modules/_uuidmodule.c index ad31c57ec72a9a9..b58cad744ac89fc 100644 --- a/Modules/_uuidmodule.c +++ b/Modules/_uuidmodule.c @@ -974,6 +974,15 @@ make_uuid(PyTypeObject *type) uuidobject *self = NULL; uuid_state *state = get_uuid_state_by_cls(type); + if (type != state->UuidType) { + // Allocate the subclass's full instance size and enable GC tracking. + self = (uuidobject *)PyType_GenericAlloc(type, 0); + if (self == NULL) { + return NULL; + } + goto init; + } + Py_BEGIN_CRITICAL_SECTION(type); if (state->freelist_size > 0) { self = state->freelist; @@ -983,8 +992,10 @@ make_uuid(PyTypeObject *type) Py_END_CRITICAL_SECTION(); if (self != NULL) { - // Reinitialize the object from freelist - _Py_NewReference((PyObject *)self); + // Deallocation releases the type reference even when caching the object, + // so the freelist does not keep the type and its module alive. Restore + // that reference and reinitialize the object before returning it. + PyObject_Init((PyObject *)self, type); } else { self = PyObject_New(uuidobject, type); @@ -993,6 +1004,7 @@ make_uuid(PyTypeObject *type) } } +init: self->is_safe = NULL; self->weakreflist = NULL; @@ -1002,7 +1014,7 @@ make_uuid(PyTypeObject *type) } static PyObject * -Uuid_alloc(PyTypeObject *type, PyObject *args, PyObject *kwargs) +Uuid_alloc(PyTypeObject *type, Py_ssize_t Py_UNUSED(nitems)) { uuidobject *self = make_uuid(type); if (self == NULL) { @@ -1044,9 +1056,10 @@ Uuid_dealloc(PyObject *obj) dealloc: if (!skip_dealloc) { type->tp_free(uuid); - // UUID is a heap allocated type so we have to decref the type ref - Py_DECREF(type); } + // Only live instances own a reference to the heap type. Keeping that + // reference in the freelist would prevent the module from being collected. + Py_DECREF(type); } From 3eff5a459e33f7bd485942093defbf9aebf36f92 Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Mon, 19 Oct 2026 15:51:07 -0700 Subject: [PATCH 18/38] gh-139122: Match UUID state restoration mapping semantics --- Lib/test/test_uuid.py | 59 ++++++++++++++++++++++++++++++++++ Modules/_uuidmodule.c | 38 +++++++++++++++------- Modules/clinic/_uuidmodule.c.h | 4 +-- 3 files changed, 88 insertions(+), 13 deletions(-) diff --git a/Lib/test/test_uuid.py b/Lib/test/test_uuid.py index 0eff0ed1fc9d827..42d5100ba8a87d2 100755 --- a/Lib/test/test_uuid.py +++ b/Lib/test/test_uuid.py @@ -10,7 +10,9 @@ import textwrap import unittest import weakref +from collections import UserDict from itertools import product +from types import MappingProxyType from unittest import mock from test import support @@ -392,6 +394,63 @@ def check(actual, expected): with self.subTest(protocol=proto): check(pickle.loads(pickle.dumps(u, proto)), u) + def test_setstate_mapping(self): + # State restoration accepts mappings and decodes serialized safety values. + safety_states = [({}, self.uuid.SafeUUID.unknown)] + safety_states.extend( + ({'is_safe': safety.value}, safety) + for safety in self.uuid.SafeUUID + ) + with support.swap_item(sys.modules, 'uuid', self.uuid): + for mapping in (dict, UserDict, MappingProxyType): + for safety_state, expected_safety in safety_states: + with self.subTest(mapping=mapping, state=safety_state): + u = self.uuid.UUID(int=0, is_safe=self.uuid.SafeUUID.safe) + state = mapping({'int': 1, **safety_state}) + self.assertIsNone(u.__setstate__(state)) + self.assertEqual(u.int, 1) + self.assertIs(u.is_safe, expected_safety) + + def test_setstate_missing_int(self): + # A missing required key must retain the mapping's KeyError and key. + for mapping in (dict, UserDict, MappingProxyType): + with self.subTest(mapping=mapping): + u = self.uuid.UUID(int=1) + with self.assertRaises(KeyError) as cm: + u.__setstate__(mapping({})) + self.assertEqual(cm.exception.args, ('int',)) + self.assertEqual(u.int, 1) + + def test_setstate_lookup_error(self): + class State(dict): + def __getitem__(self, key): + if key == bad_key: + raise RuntimeError('lookup failed') + return super().__getitem__(key) + + # Honor overridden lookups, including on dict subclasses. + for bad_key in ('int', 'is_safe'): + with self.subTest(key=bad_key): + u = self.uuid.UUID(int=1) + with self.assertRaisesRegex(RuntimeError, 'lookup failed'): + u.__setstate__(State(int=2, is_safe=0)) + + def test_setstate_contains_error(self): + class State(dict): + def __contains__(self, key): + raise RuntimeError('membership test failed') + + # Errors checking for the optional key must propagate as well. + u = self.uuid.UUID(int=1) + with self.assertRaisesRegex(RuntimeError, 'membership test failed'): + u.__setstate__(State(int=2)) + + def test_setstate_invalid_is_safe(self): + u = self.uuid.UUID(int=0) + with support.swap_item(sys.modules, 'uuid', self.uuid): + with self.assertRaisesRegex(ValueError, '42 is not a valid SafeUUID'): + u.__setstate__({'int': 1, 'is_safe': 42}) + def test_unpickle_previous_python_versions(self): def check(actual, expected): self.assertEqual(actual, expected) diff --git a/Modules/_uuidmodule.c b/Modules/_uuidmodule.c index b58cad744ac89fc..33cf13352ed4e42 100644 --- a/Modules/_uuidmodule.c +++ b/Modules/_uuidmodule.c @@ -1616,33 +1616,49 @@ _uuid.UUID.__setstate__ Restore the UUID's state from pickling. -Expects a dictionary with 'int' and optionally 'is_safe' keys. +Expects a mapping with 'int' and optionally 'is_safe' keys. [clinic start generated code]*/ static PyObject * _uuid_UUID___setstate___impl(uuidobject *self, PyObject *state) -/*[clinic end generated code: output=cdf6bd4a2a680b3f input=b1ec0744788a73a0]*/ +/*[clinic end generated code: output=cdf6bd4a2a680b3f input=fc65bdad0a8d6211]*/ { - if (!PyDict_Check(state)) { - PyErr_SetString(PyExc_TypeError, "state must be a dictionary"); + // Get and set the 'int' value + PyObject *int_value = PyObject_GetItem(state, &_Py_ID(int)); + if (int_value == NULL) { return NULL; } - // Get and set the 'int' value - PyObject *int_value = PyDict_GetItem(state, &_Py_ID(int)); - if (int_value == NULL) { - PyErr_SetString(PyExc_ValueError, "state must have 'int' key"); + int result = from_int(self, int_value, 1); + Py_DECREF(int_value); + if (result < 0) { return NULL; } - if (from_int(self, int_value, 1) < 0) { + int has_is_safe = PySequence_Contains(state, &_Py_ID(is_safe)); + if (has_is_safe < 0) { + return NULL; + } + PyObject *is_safe = has_is_safe + ? PyObject_GetItem(state, &_Py_ID(is_safe)) + : Py_NewRef(Py_None); + if (is_safe == NULL) { return NULL; } - Py_CLEAR(self->is_safe); - if (PyDict_GetItemRef(state, &_Py_ID(is_safe), &self->is_safe) < 0) { + uuid_state *module_state = get_uuid_state_by_cls(Py_TYPE(self)); + PyObject *SafeUUID = get_SafeUUID(module_state); + if (SafeUUID == NULL) { + Py_DECREF(is_safe); + return NULL; + } + PyObject *safety = PyObject_CallOneArg(SafeUUID, is_safe); + Py_DECREF(SafeUUID); + Py_DECREF(is_safe); + if (safety == NULL) { return NULL; } + Py_XSETREF(self->is_safe, safety); Py_RETURN_NONE; } diff --git a/Modules/clinic/_uuidmodule.c.h b/Modules/clinic/_uuidmodule.c.h index f8fffa9b41b7b7a..7481d3cd18affe4 100644 --- a/Modules/clinic/_uuidmodule.c.h +++ b/Modules/clinic/_uuidmodule.c.h @@ -297,7 +297,7 @@ PyDoc_STRVAR(_uuid_UUID___setstate____doc__, "\n" "Restore the UUID\'s state from pickling.\n" "\n" -"Expects a dictionary with \'int\' and optionally \'is_safe\' keys."); +"Expects a mapping with \'int\' and optionally \'is_safe\' keys."); #define _UUID_UUID___SETSTATE___METHODDEF \ {"__setstate__", (PyCFunction)_uuid_UUID___setstate__, METH_O, _uuid_UUID___setstate____doc__}, @@ -314,4 +314,4 @@ _uuid_UUID___setstate__(PyObject *self, PyObject *state) return return_value; } -/*[clinic end generated code: output=095610812af4b3bd input=a9049054013a1b77]*/ +/*[clinic end generated code: output=06f9a312bde5b746 input=a9049054013a1b77]*/ From 634151a16073c2d0899a07f78ea27628dd4fc587 Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Mon, 19 Oct 2026 16:00:39 -0700 Subject: [PATCH 19/38] gh-139122: Fix UUID time hook error handling and reference updates --- Lib/test/test_uuid.py | 36 ++++++++++++++++++++++++++++++++++++ Modules/_uuidmodule.c | 21 +++++++++++---------- 2 files changed, 47 insertions(+), 10 deletions(-) diff --git a/Lib/test/test_uuid.py b/Lib/test/test_uuid.py index 42d5100ba8a87d2..fb89b26a1e816db 100755 --- a/Lib/test/test_uuid.py +++ b/Lib/test/test_uuid.py @@ -1746,6 +1746,42 @@ def _reset_hooks(self, uuid_mod): uuid_mod._install_c_hooks(random_func=None, time_func=None) uuid_mod._install_py_hooks(random_func=None, time_func=None) + def test_invalid_time_hook_result(self): + import uuid + + class IntSubclass(int): + pass + + class Indexable: + def __index__(self): + raise AssertionError('invalid results must not be converted') + + # Invalid results must be rejected before integer conversion. + for value in (None, False, 1.0, '1', object(), + IntSubclass(1), Indexable()): + with self.subTest(value=value): + uuid._install_c_hooks(random_func=None, + time_func=lambda: value) + try: + with self.assertRaisesRegex(ValueError, + '^time_func must return int$'): + uuid.uuid7() + finally: + uuid._install_c_hooks(random_func=None, time_func=None) + + def test_time_hook_exception(self): + import uuid + + def time_func(): + raise RuntimeError('time hook failed') + + uuid._install_c_hooks(random_func=None, time_func=time_func) + try: + with self.assertRaisesRegex(RuntimeError, 'time hook failed'): + uuid.uuid7() + finally: + uuid._install_c_hooks(random_func=None, time_func=None) + def test_exact_same_algo_uuid4(self): import uuid diff --git a/Modules/_uuidmodule.c b/Modules/_uuidmodule.c index 33cf13352ed4e42..f2a9b9e51b41558 100644 --- a/Modules/_uuidmodule.c +++ b/Modules/_uuidmodule.c @@ -238,7 +238,7 @@ get_uuid_state_by_cls(PyTypeObject *cls) static PyObject *uuid_from_bytes_array(PyTypeObject *type, uint8_t bytes[16]); static int -gen_time(uuid_state *state, PyTime_t* time) +gen_time(uuid_state *state, PyTime_t *time) { if (state->time_func == NULL) { return PyTime_Time(time); @@ -250,7 +250,9 @@ gen_time(uuid_state *state, PyTime_t* time) } if (!PyLong_CheckExact(ret)) { - PyErr_SetString(PyExc_ValueError, "random_time must return int"); + PyErr_SetString(PyExc_ValueError, "time_func must return int"); + Py_DECREF(ret); + return -1; } int res = PyLong_AsInt64(ret, time); @@ -484,16 +486,16 @@ _uuid__install_c_hooks_impl(PyObject *module, PyObject *random_func, if (random_func == Py_None) { Py_CLEAR(state->random_func); - } else { - Py_INCREF(random_func); - Py_XSETREF(state->random_func, random_func); + } + else { + Py_XSETREF(state->random_func, Py_NewRef(random_func)); } if (time_func == Py_None) { Py_CLEAR(state->time_func); - } else { - Py_INCREF(time_func); - Py_XSETREF(state->time_func, time_func); + } + else { + Py_XSETREF(state->time_func, Py_NewRef(time_func)); } Py_RETURN_NONE; @@ -595,8 +597,7 @@ _uuid_UUID___init___impl(uuidobject *self, PyObject *hex, Py_buffer *bytes, is_safe = NULL; } if (is_safe != NULL) { - Py_INCREF(is_safe); - Py_XSETREF(self->is_safe, is_safe); + Py_XSETREF(self->is_safe, Py_NewRef(is_safe)); } return 0; From 5627dbea967eadf6c7577dc7a84af95cc2024158 Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Mon, 19 Oct 2026 16:23:32 -0700 Subject: [PATCH 20/38] gh-139122: Align C UUID constructor inputs with Python --- Doc/library/uuid.rst | 5 ++ Doc/whatsnew/3.16.rst | 6 ++ Lib/test/test_uuid.py | 46 +++++++++++++ Lib/uuid.py | 10 +-- ...-10-19-16-00-00.gh-issue-139122.bYtEs1.rst | 3 + Modules/_uuidmodule.c | 69 ++++++++++--------- Modules/clinic/_uuidmodule.c.h | 46 ++++--------- 7 files changed, 112 insertions(+), 73 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-10-19-16-00-00.gh-issue-139122.bYtEs1.rst diff --git a/Doc/library/uuid.rst b/Doc/library/uuid.rst index f1c49f8a94c174d..6e21b6f99f877d4 100644 --- a/Doc/library/uuid.rst +++ b/Doc/library/uuid.rst @@ -69,6 +69,11 @@ which relays any information about the UUID's safety, using this enumeration: variant and version number set according to :rfc:`9562`, overriding bits in the given *hex*, *bytes*, *bytes_le*, *fields*, or *int*. + .. versionchanged:: 3.16 + Passing an object other than :class:`bytes` as *bytes* or *bytes_le* + now consistently raises :exc:`TypeError`. Previously, some invalid inputs + raised :exc:`AssertionError` or :exc:`ValueError`. + Comparison of UUID objects are made by way of comparing their :attr:`UUID.int` attributes. Comparison with a non-UUID object raises a :exc:`TypeError`. diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index b29170b5b0b93da..24e2df53a28a717 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -1151,6 +1151,12 @@ that may require changes to your code. :exc:`TypeError`. (Contributed by Serhiy Storchaka in :gh:`152587`.) +* :class:`uuid.UUID` now consistently raises :exc:`TypeError` when the + *bytes* or *bytes_le* argument is not a :class:`bytes` object. + Previously, some invalid inputs raised :exc:`AssertionError` or + :exc:`ValueError`. + (Contributed by Yury Selivanov in :gh:`139123`.) + * :mod:`xml.dom.minidom` now raises :exc:`~xml.dom.InvalidCharacterErr` for a name which is not a valid XML name, :exc:`~xml.dom.WrongDocumentErr` diff --git a/Lib/test/test_uuid.py b/Lib/test/test_uuid.py index fb89b26a1e816db..dcf0b28885be0ef 100755 --- a/Lib/test/test_uuid.py +++ b/Lib/test/test_uuid.py @@ -371,6 +371,52 @@ def test_exceptions(self): badtype(lambda: u < object()) badtype(lambda: u > object()) + def test_constructor_none(self): + # Explicit None has the same meaning as omitting an input argument. + expected = self.uuid.UUID('12345678-1234-5678-1234-567812345678') + inputs = { + 'hex': expected.hex, + 'bytes': expected.bytes, + 'bytes_le': expected.bytes_le, + 'fields': expected.fields, + 'int': expected.int, + } + for name, value in inputs.items(): + with self.subTest(input=name): + kwargs = dict.fromkeys(inputs) + kwargs[name] = value + self.assertEqual(self.uuid.UUID(**kwargs, version=None), expected) + self.assertEqual( + self.uuid.UUID(*kwargs.values(), version=None), expected) + + with self.assertRaisesRegex(TypeError, 'one of the hex, bytes, bytes_le'): + self.uuid.UUID(**dict.fromkeys(inputs)) + + def test_constructor_bytes_type(self): + # A buffer interface alone does not satisfy the bytes arguments. + for name in ('bytes', 'bytes_le'): + for value in (bytearray(16), memoryview(bytes(16)), 'x' * 16, + [0] * 16, 16, object()): + with self.subTest(argument=name, type=type(value)): + with self.assertRaisesRegex( + TypeError, f'^{name} must be a bytes object$' + ): + self.uuid.UUID(**{name: value}) + + def test_constructor_bytes_subclass(self): + class BytesSubclass(bytes): + pass + + expected = self.uuid.UUID('12345678-1234-5678-1234-567812345678') + for name in ('bytes', 'bytes_le'): + with self.subTest(argument=name): + value = BytesSubclass(getattr(expected, name)) + self.assertEqual(self.uuid.UUID(**{name: value}), expected) + + def test_constructor_is_safe_none(self): + # is_safe is stored as supplied; None is distinct from SafeUUID.unknown. + self.assertIsNone(self.uuid.UUID(int=1, is_safe=None).is_safe) + def test_getnode(self): node1 = self.uuid.getnode() self.assertTrue(0 < node1 < (1 << 48), '%012x' % node1) diff --git a/Lib/uuid.py b/Lib/uuid.py index cd1feb2e9e3aa6b..5766f05ed232ae4 100644 --- a/Lib/uuid.py +++ b/Lib/uuid.py @@ -261,23 +261,17 @@ def __init__(self, hex=None, bytes=None, bytes_le=None, fields=None, int = int_(hex, 16) elif bytes_le is not None: if not isinstance(bytes_le, bytes_): - raise TypeError( - f'a bytes-like object is required, not {type(bytes_le).__name__!r}' - ) + raise TypeError('bytes_le must be a bytes object') if len(bytes_le) != 16: raise ValueError('bytes_le is not a 16-char string') - assert isinstance(bytes_le, bytes_), repr(bytes_le) bytes = (bytes_le[4-1::-1] + bytes_le[6-1:4-1:-1] + bytes_le[8-1:6-1:-1] + bytes_le[8:]) int = int_.from_bytes(bytes) # big endian elif bytes is not None: if not isinstance(bytes, bytes_): - raise TypeError( - f'a bytes-like object is required, not {type(bytes).__name__!r}' - ) + raise TypeError('bytes must be a bytes object') if len(bytes) != 16: raise ValueError('bytes is not a 16-char string') - assert isinstance(bytes, bytes_), repr(bytes) int = int_.from_bytes(bytes) # big endian elif fields is not None: if len(fields) != 6: diff --git a/Misc/NEWS.d/next/Library/2026-10-19-16-00-00.gh-issue-139122.bYtEs1.rst b/Misc/NEWS.d/next/Library/2026-10-19-16-00-00.gh-issue-139122.bYtEs1.rst new file mode 100644 index 000000000000000..256b36941aa910d --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-19-16-00-00.gh-issue-139122.bYtEs1.rst @@ -0,0 +1,3 @@ +:class:`uuid.UUID` now consistently raises :exc:`TypeError` when the *bytes* +or *bytes_le* argument is not a :class:`bytes` object. Previously, some invalid +inputs raised :exc:`AssertionError` or :exc:`ValueError`. diff --git a/Modules/_uuidmodule.c b/Modules/_uuidmodule.c index f2a9b9e51b41558..518dba5c64d06c6 100644 --- a/Modules/_uuidmodule.c +++ b/Modules/_uuidmodule.c @@ -200,7 +200,7 @@ class _uuid.UUID "uuidobject *" "&UuidType" // Forward declarations static int from_hex(uuidobject *self, PyObject *hex); -static int from_bytes_le(uuidobject *self, Py_buffer *bytes_le); +static int from_bytes_le(uuidobject *self, PyObject *bytes_le); static int from_int(uuidobject *self, PyObject *int_value, int validate); static int from_fields(uuidobject *self, PyObject *fields); static PyObject * get_SafeUUID(uuid_state *state); @@ -504,12 +504,12 @@ _uuid__install_c_hooks_impl(PyObject *module, PyObject *random_func, /*[clinic input] _uuid.UUID.__init__ - hex: 'U' = NULL - bytes: 'y*' = None - bytes_le: 'y*' = None - fields: object = NULL - int: object = NULL - version: object = NULL + hex: object = None + bytes: object = None + bytes_le: object = None + fields: object = None + int: object = None + version: object = None * is_safe: object = NULL @@ -517,19 +517,20 @@ UUID is a fast base implementation type for uuid.UUID. [clinic start generated code]*/ static int -_uuid_UUID___init___impl(uuidobject *self, PyObject *hex, Py_buffer *bytes, - Py_buffer *bytes_le, PyObject *fields, +_uuid_UUID___init___impl(uuidobject *self, PyObject *hex, PyObject *bytes, + PyObject *bytes_le, PyObject *fields, PyObject *int_value, PyObject *version, PyObject *is_safe) -/*[clinic end generated code: output=93a6881c8f79bf9b input=b9c79672fbd76a99]*/ +/*[clinic end generated code: output=6d100803f5ecd751 input=23c754454cc11724]*/ { + // Explicit None values do not select a UUID input form. int passed = 0; - if (hex != NULL) passed++; - if (bytes->obj != NULL) passed++; - if (bytes_le->obj != NULL) passed++; - if (fields != NULL) passed++; - if (int_value != NULL) passed++; + if (hex != Py_None) passed++; + if (bytes != Py_None) passed++; + if (bytes_le != Py_None) passed++; + if (fields != Py_None) passed++; + if (int_value != Py_None) passed++; if (passed != 1) { PyErr_SetString( PyExc_TypeError, @@ -538,32 +539,36 @@ _uuid_UUID___init___impl(uuidobject *self, PyObject *hex, Py_buffer *bytes, return -1; } - if (hex != NULL) { + if (hex != Py_None) { if (from_hex(self, hex) < 0) { return -1; } } - else if (bytes->obj != NULL) { - if (bytes->len != 16) { + else if (bytes != Py_None) { + if (!PyBytes_Check(bytes)) { + PyErr_SetString(PyExc_TypeError, "bytes must be a bytes object"); + return -1; + } + if (PyBytes_GET_SIZE(bytes) != 16) { PyErr_SetString( PyExc_ValueError, "bytes is not a 16-char string" ); return -1; } - memcpy(self->bytes, bytes->buf, 16); + memcpy(self->bytes, PyBytes_AS_STRING(bytes), 16); } - else if (bytes_le->obj != NULL) { + else if (bytes_le != Py_None) { if (from_bytes_le(self, bytes_le) < 0) { return -1; } } - else if (fields != NULL) { + else if (fields != Py_None) { if (from_fields(self, fields) < 0) { return -1; } } - else if (int_value != NULL) { + else if (int_value != Py_None) { if (from_int(self, int_value, 1) < 0) { return -1; } @@ -572,7 +577,7 @@ _uuid_UUID___init___impl(uuidobject *self, PyObject *hex, Py_buffer *bytes, Py_UNREACHABLE(); } - if (version != NULL && version != Py_None) { + if (version != Py_None) { long version_num = PyLong_AsLong(version); if (version_num == -1 && PyErr_Occurred()) { return -1; @@ -592,13 +597,7 @@ _uuid_UUID___init___impl(uuidobject *self, PyObject *hex, Py_buffer *bytes, self->bytes[6] |= (version_num << 4); } - if (is_safe == Py_None) { - // Py_None is immortal; skip decref - is_safe = NULL; - } - if (is_safe != NULL) { - Py_XSETREF(self->is_safe, Py_NewRef(is_safe)); - } + Py_XSETREF(self->is_safe, Py_XNewRef(is_safe)); return 0; } @@ -725,9 +724,13 @@ from_hex(uuidobject *self, PyObject *hex) } static int -from_bytes_le(uuidobject *self, Py_buffer *bytes_le) +from_bytes_le(uuidobject *self, PyObject *bytes_le) { - if (bytes_le->len != 16) { + if (!PyBytes_Check(bytes_le)) { + PyErr_SetString(PyExc_TypeError, "bytes_le must be a bytes object"); + return -1; + } + if (PyBytes_GET_SIZE(bytes_le) != 16) { PyErr_SetString(PyExc_ValueError, "bytes_le is not a 16-char string"); return -1; @@ -742,7 +745,7 @@ from_bytes_le(uuidobject *self, Py_buffer *bytes_le) // - clock_seq_low (1 byte) - no swap needed // - node (6 bytes) - no swap needed - unsigned char *src = (unsigned char *)bytes_le->buf; + const unsigned char *src = (const unsigned char *)PyBytes_AS_STRING(bytes_le); unsigned char *dst = (unsigned char *)self->bytes; // Swap time_low (bytes 0-3) diff --git a/Modules/clinic/_uuidmodule.c.h b/Modules/clinic/_uuidmodule.c.h index 7481d3cd18affe4..cd7643846ca91af 100644 --- a/Modules/clinic/_uuidmodule.c.h +++ b/Modules/clinic/_uuidmodule.c.h @@ -122,16 +122,15 @@ _uuid__install_c_hooks(PyObject *module, PyObject *const *args, Py_ssize_t nargs } PyDoc_STRVAR(_uuid_UUID___init____doc__, -"UUID(hex=, bytes=None, bytes_le=None,\n" -" fields=, int=,\n" -" version=, *, is_safe=)\n" +"UUID(hex=None, bytes=None, bytes_le=None, fields=None, int=None,\n" +" version=None, *, is_safe=)\n" "--\n" "\n" "UUID is a fast base implementation type for uuid.UUID."); static int -_uuid_UUID___init___impl(uuidobject *self, PyObject *hex, Py_buffer *bytes, - Py_buffer *bytes_le, PyObject *fields, +_uuid_UUID___init___impl(uuidobject *self, PyObject *hex, PyObject *bytes, + PyObject *bytes_le, PyObject *fields, PyObject *int_value, PyObject *version, PyObject *is_safe); @@ -170,12 +169,12 @@ _uuid_UUID___init__(PyObject *self, PyObject *args, PyObject *kwargs) PyObject * const *fastargs; Py_ssize_t nargs = PyTuple_GET_SIZE(args); Py_ssize_t noptargs = nargs + (kwargs ? PyDict_GET_SIZE(kwargs) : 0) - 0; - PyObject *hex = NULL; - Py_buffer bytes = {NULL, NULL}; - Py_buffer bytes_le = {NULL, NULL}; - PyObject *fields = NULL; - PyObject *int_value = NULL; - PyObject *version = NULL; + PyObject *hex = Py_None; + PyObject *bytes = Py_None; + PyObject *bytes_le = Py_None; + PyObject *fields = Py_None; + PyObject *int_value = Py_None; + PyObject *version = Py_None; PyObject *is_safe = NULL; fastargs = _PyArg_UnpackKeywords(_PyTuple_CAST(args)->ob_item, nargs, kwargs, NULL, &_parser, @@ -187,27 +186,19 @@ _uuid_UUID___init__(PyObject *self, PyObject *args, PyObject *kwargs) goto skip_optional_pos; } if (fastargs[0]) { - if (!PyUnicode_Check(fastargs[0])) { - _PyArg_BadArgument("UUID", "argument 'hex'", "str", fastargs[0]); - goto exit; - } hex = fastargs[0]; if (!--noptargs) { goto skip_optional_pos; } } if (fastargs[1]) { - if (PyObject_GetBuffer(fastargs[1], &bytes, PyBUF_SIMPLE) != 0) { - goto exit; - } + bytes = fastargs[1]; if (!--noptargs) { goto skip_optional_pos; } } if (fastargs[2]) { - if (PyObject_GetBuffer(fastargs[2], &bytes_le, PyBUF_SIMPLE) != 0) { - goto exit; - } + bytes_le = fastargs[2]; if (!--noptargs) { goto skip_optional_pos; } @@ -236,18 +227,9 @@ _uuid_UUID___init__(PyObject *self, PyObject *args, PyObject *kwargs) } is_safe = fastargs[6]; skip_optional_kwonly: - return_value = _uuid_UUID___init___impl((uuidobject *)self, hex, &bytes, &bytes_le, fields, int_value, version, is_safe); + return_value = _uuid_UUID___init___impl((uuidobject *)self, hex, bytes, bytes_le, fields, int_value, version, is_safe); exit: - /* Cleanup for bytes */ - if (bytes.obj) { - PyBuffer_Release(&bytes); - } - /* Cleanup for bytes_le */ - if (bytes_le.obj) { - PyBuffer_Release(&bytes_le); - } - return return_value; } @@ -314,4 +296,4 @@ _uuid_UUID___setstate__(PyObject *self, PyObject *state) return return_value; } -/*[clinic end generated code: output=06f9a312bde5b746 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=929e54bf30e649aa input=a9049054013a1b77]*/ From f1b2eaefd38f2abd0f12769b50a15dcda344d8ad Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Mon, 19 Oct 2026 16:40:56 -0700 Subject: [PATCH 21/38] gh-139122: Make UUID narrowing and entropy lock requirements explicit --- Modules/_uuidmodule.c | 111 +++++++++++++++++++++--------------------- 1 file changed, 56 insertions(+), 55 deletions(-) diff --git a/Modules/_uuidmodule.c b/Modules/_uuidmodule.c index 518dba5c64d06c6..cfda933b4a1bdcf 100644 --- a/Modules/_uuidmodule.c +++ b/Modules/_uuidmodule.c @@ -18,6 +18,7 @@ #include // for strncasecmp #include "structmember.h" // for PyMemberDef +#include "pycore_critical_section.h" // _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED() #include "pycore_long.h" // _PyLong_FromByteArray, _PyLong_AsByteArray #include "pycore_pylifecycle.h" // _PyOS_URandom() #include "pycore_time.h" // PyTime_Time @@ -206,8 +207,9 @@ static int from_fields(uuidobject *self, PyObject *fields); static PyObject * get_SafeUUID(uuid_state *state); static uint64_t -uuid_getpid(void) { - #if !defined(MS_WINDOWS) || defined(MS_WINDOWS_DESKTOP) || defined(MS_WINDOWS_SYSTEM) +uuid_getpid(void) +{ +#if !defined(MS_WINDOWS) || defined(MS_WINDOWS_DESKTOP) || defined(MS_WINDOWS_SYSTEM) return (uint64_t)getpid(); #else return (uint64_t)GetCurrentProcessId(); @@ -261,15 +263,15 @@ gen_time(uuid_state *state, PyTime_t *time) } static int -gen_random(uuid_state *state, uint8_t *bytes, Py_ssize_t size) +gen_random_lock_held(PyObject *module, uuid_state *state, uint8_t *bytes, + Py_ssize_t size) { + _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(module); + // Overfetching & caching entropy improves the performance 10x. // There's a precedent with NodeJS doing exact same thing for // improving performance of their UUID implementation. - // IMPORTANT: callers should have a critical section or a lock - // around this function. - uint64_t pid = uuid_getpid(); if (pid != state->random_last_pid) { // The main concern to take core of with caching entropy is handling @@ -352,22 +354,23 @@ _uuid_uuid4_impl(PyObject *module) uuid_state *state = get_uuid_state(module); uint8_t bytes[16]; - if (gen_random(state, bytes, 16) < 0) { + if (gen_random_lock_held(module, state, bytes, 16) < 0) { return NULL; } // Set version (4) and variant - bytes[6] = (bytes[6] & 0x0f) | 0x40; - bytes[8] = (bytes[8] & 0x3f) | 0x80; + bytes[6] = (uint8_t)((bytes[6] & 0x0f) | 0x40); + bytes[8] = (uint8_t)((bytes[8] & 0x3f) | 0x80); return uuid_from_bytes_array(state->UuidType, bytes); } static inline int -uuid7_get_counter_and_tail(uuid_state *state, uint64_t *counter, uint8_t *tail) +uuid7_get_counter_and_tail(PyObject *module, uuid_state *state, + uint64_t *counter, uint8_t *tail) { uint8_t rand_bytes[10]; - if (gen_random(state, rand_bytes, 10) < 0) { + if (gen_random_lock_held(module, state, rand_bytes, 10) < 0) { return -1; } @@ -410,7 +413,7 @@ _uuid_uuid7_impl(PyObject *module) timestamp_ms = (uint64_t)(pytime / 1000000); if (!state->last_timestamp_v7_init || timestamp_ms > state->last_timestamp_v7) { - if (uuid7_get_counter_and_tail(state, &counter, bytes + 12) < 0) { + if (uuid7_get_counter_and_tail(module, state, &counter, bytes + 12) < 0) { return NULL; } } else { @@ -422,37 +425,37 @@ _uuid_uuid7_impl(PyObject *module) if (counter > 0x3FFFFFFFFFF) { // advance the 48-bit timestamp timestamp_ms += 1; - if (uuid7_get_counter_and_tail(state, &counter, bytes + 12) < 0) { + if (uuid7_get_counter_and_tail(module, state, &counter, bytes + 12) < 0) { return NULL; } } else { // This is the common fast path, we only need 4 bytes of entropy // 32-bit random data - if (gen_random(state, bytes + 12, 4) < 0) { + if (gen_random_lock_held(module, state, bytes + 12, 4) < 0) { return NULL; } } } timestamp_ms &= 0xFFFFFFFFFFFF; - bytes[0] = (timestamp_ms >> 40); - bytes[1] = (timestamp_ms >> 32); - bytes[2] = (timestamp_ms >> 24); - bytes[3] = (timestamp_ms >> 16); - bytes[4] = (timestamp_ms >> 8); - bytes[5] = timestamp_ms; + bytes[0] = (uint8_t)(timestamp_ms >> 40); + bytes[1] = (uint8_t)(timestamp_ms >> 32); + bytes[2] = (uint8_t)(timestamp_ms >> 24); + bytes[3] = (uint8_t)(timestamp_ms >> 16); + bytes[4] = (uint8_t)(timestamp_ms >> 8); + bytes[5] = (uint8_t)timestamp_ms; - uint16_t counter_hi = (counter >> 30) & 0x0FFF; - bytes[6] = 0x70 | ((counter_hi >> 8)); // Version 7 = 0111 - bytes[7] = counter_hi; + uint16_t counter_hi = (uint16_t)((counter >> 30) & 0x0FFF); + bytes[6] = (uint8_t)(0x70 | (counter_hi >> 8)); // Version 7 = 0111 + bytes[7] = (uint8_t)counter_hi; - uint16_t counter_mid = (counter >> 16) & 0x3FFF; - bytes[8] = 0x80 | (counter_mid >> 8); // Variant = 10 - bytes[9] = counter_mid; + uint16_t counter_mid = (uint16_t)((counter >> 16) & 0x3FFF); + bytes[8] = (uint8_t)(0x80 | (counter_mid >> 8)); // Variant = 10 + bytes[9] = (uint8_t)counter_mid; - uint16_t counter_lo = counter & 0xFFFF; - bytes[10] = counter_lo >> 8; - bytes[11] = counter_lo; + uint16_t counter_lo = (uint16_t)(counter & 0xFFFF); + bytes[10] = (uint8_t)(counter_lo >> 8); + bytes[11] = (uint8_t)counter_lo; state->last_timestamp_v7_init = 1; state->last_timestamp_v7 = timestamp_ms; @@ -594,7 +597,7 @@ _uuid_UUID___init___impl(uuidobject *self, PyObject *hex, PyObject *bytes, // Set the variant to RFC 4122/9562 (binary 10xx xxxx) self->bytes[8] |= 0x80; // 1000 0000 // Set the version number (upper 4 bits of byte 6) - self->bytes[6] |= (version_num << 4); + self->bytes[6] |= (uint8_t)(version_num << 4); } Py_XSETREF(self->is_safe, Py_XNewRef(is_safe)); @@ -701,7 +704,7 @@ from_hex(uuidobject *self, PyObject *hex) j++; } else { - acc = (uint8_t)part << 4; + acc = (uint8_t)(part << 4); acc_set = 1; } @@ -914,27 +917,27 @@ from_fields(uuidobject *self, PyObject *fields) uint64_t, node ); - self->bytes[0] = time_low >> 24; - self->bytes[1] = time_low >> 16; - self->bytes[2] = time_low >> 8; - self->bytes[3] = time_low; + self->bytes[0] = (uint8_t)(time_low >> 24); + self->bytes[1] = (uint8_t)(time_low >> 16); + self->bytes[2] = (uint8_t)(time_low >> 8); + self->bytes[3] = (uint8_t)time_low; - self->bytes[4] = time_mid >> 8; - self->bytes[5] = time_mid; + self->bytes[4] = (uint8_t)(time_mid >> 8); + self->bytes[5] = (uint8_t)time_mid; - self->bytes[6] = time_hi_version >> 8; - self->bytes[7] = time_hi_version; + self->bytes[6] = (uint8_t)(time_hi_version >> 8); + self->bytes[7] = (uint8_t)time_hi_version; self->bytes[8] = clock_seq_hi_variant; self->bytes[9] = clock_seq_low; - self->bytes[10] = node >> 40; - self->bytes[11] = node >> 32; - self->bytes[12] = node >> 24; - self->bytes[13] = node >> 16; - self->bytes[14] = node >> 8; - self->bytes[15] = node; + self->bytes[10] = (uint8_t)(node >> 40); + self->bytes[11] = (uint8_t)(node >> 32); + self->bytes[12] = (uint8_t)(node >> 24); + self->bytes[13] = (uint8_t)(node >> 16); + self->bytes[14] = (uint8_t)(node >> 8); + self->bytes[15] = (uint8_t)node; return 0; } @@ -1168,15 +1171,13 @@ get_time_low(uuidobject *self) static inline uint16_t get_time_mid(uuidobject *self) { - return ((uint16_t)self->bytes[4] << 8) | - ((uint16_t)self->bytes[5]); + return (uint16_t)((self->bytes[4] << 8) | self->bytes[5]); } static inline uint16_t get_time_hi_version(uuidobject *self) { - return ((uint16_t)self->bytes[6] << 8) | - ((uint16_t)self->bytes[7]); + return (uint16_t)((self->bytes[6] << 8) | self->bytes[7]); } static inline uint8_t @@ -1248,8 +1249,8 @@ Uuid_get_time(PyObject *o, void *closure) // UUID v6: time_hi (32) | time_mid (16) | ver (4) | time_lo (12) | ... (64) uint32_t time_hi = get_time_low(self); uint16_t time_mid = get_time_mid(self); - uint16_t time_lo = ((uint16_t)(self->bytes[6] & 0x0f) << 8) | - ((uint16_t)self->bytes[7]); + uint16_t time_lo = (uint16_t)(((self->bytes[6] & 0x0f) << 8) | + self->bytes[7]); uint64_t time = ((uint64_t)time_hi << 28) | ((uint64_t)time_mid << 12) | @@ -1272,8 +1273,8 @@ Uuid_get_time(PyObject *o, void *closure) // | time_hi (12) | ... (64) uint32_t time_lo = get_time_low(self); uint16_t time_mid = get_time_mid(self); - uint16_t time_hi = ((uint16_t)(self->bytes[6] & 0x0f) << 8) | - ((uint16_t)self->bytes[7]); + uint16_t time_hi = (uint16_t)(((self->bytes[6] & 0x0f) << 8) | + self->bytes[7]); uint64_t time = ((uint64_t)time_hi << 48) | ((uint64_t)time_mid << 32) | @@ -1287,8 +1288,8 @@ Uuid_get_clock_seq(PyObject *o, void *closure) { uuidobject *self = (uuidobject *)o; // clock_seq_hi_variant (byte 8) & 0x3f, then clock_seq_low (byte 9) - uint16_t clock_seq = ((uint16_t)(get_clock_seq_hi_variant(self) & 0x3f) << 8) | - ((uint16_t)get_clock_seq_low(self)); + uint16_t clock_seq = (uint16_t)(((get_clock_seq_hi_variant(self) & 0x3f) << 8) | + get_clock_seq_low(self)); return PyLong_FromUnsignedLong(clock_seq); } From b86a52f88fed08de59f83df0fceb7f248694f080 Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Mon, 19 Oct 2026 17:01:09 -0700 Subject: [PATCH 22/38] gh-139122: Preserve UUID fields length errors --- Lib/test/test_uuid.py | 18 ++++++++++++++++++ Modules/_uuidmodule.c | 8 +++----- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_uuid.py b/Lib/test/test_uuid.py index dcf0b28885be0ef..5aac4a2f5e58101 100755 --- a/Lib/test/test_uuid.py +++ b/Lib/test/test_uuid.py @@ -417,6 +417,24 @@ def test_constructor_is_safe_none(self): # is_safe is stored as supplied; None is distinct from SafeUUID.unknown. self.assertIsNone(self.uuid.UUID(int=1, is_safe=None).is_safe) + def test_constructor_fields_length_error(self): + error = RuntimeError('fields length failed') + + class Fields(tuple): + def __len__(self): + raise error + + class NonSequence: + def __len__(self): + raise error + + # Preserve __len__ errors, including for objects without __getitem__. + for fields in (Fields((0,) * 6), NonSequence()): + with self.subTest(type=type(fields)): + with self.assertRaises(RuntimeError) as cm: + self.uuid.UUID(fields=fields) + self.assertIs(cm.exception, error) + def test_getnode(self): node1 = self.uuid.getnode() self.assertTrue(0 < node1 < (1 << 48), '%012x' % node1) diff --git a/Modules/_uuidmodule.c b/Modules/_uuidmodule.c index cfda933b4a1bdcf..8dd56da908b8990 100644 --- a/Modules/_uuidmodule.c +++ b/Modules/_uuidmodule.c @@ -869,13 +869,11 @@ extract_field( static int from_fields(uuidobject *self, PyObject *fields) { - // Validate that fields is a sequence with exactly 6 elements - if (!PySequence_Check(fields)) { - PyErr_SetString(PyExc_TypeError, "fields must be a sequence"); + // Validate the length without replacing errors raised by __len__. + Py_ssize_t len = PySequence_Size(fields); + if (len < 0) { return -1; } - - Py_ssize_t len = PySequence_Size(fields); if (len != 6) { PyErr_SetString(PyExc_ValueError, "fields is not a 6-tuple"); return -1; From fdfaefe5250ddd4feae2bd110bd1142a8a1d3d41 Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Mon, 19 Oct 2026 17:13:16 -0700 Subject: [PATCH 23/38] gh-139122: Clarify version-specific UUID layouts --- Modules/_uuidmodule.c | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/Modules/_uuidmodule.c b/Modules/_uuidmodule.c index 8dd56da908b8990..5e6d67d4edc6945 100644 --- a/Modules/_uuidmodule.c +++ b/Modules/_uuidmodule.c @@ -140,22 +140,23 @@ typedef struct uuidobject { // // String: xx xx xx xx - xx xx - Mx xx - Nx xx - xx xx xx xx xx xx // Byte pos: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 -// ^^^^^^^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^^^^^^^^^^^^^ -// time_low mid hi seq node // -// Byte Layout (big-endian): +// Time-based layouts in big-endian order (field widths in bits): // -// Bytes 0-3: time_low (32 bits) -// Bytes 4-5: time_mid (16 bits) -// Bytes 6-7: time_hi_and_version (16 bits) -// Bytes 8-9: clock_seq_and_variant (16 bits) -// Bytes 10-15: node (48 bits) +// v1: time_low(32) | time_mid(16) | version(4) | time_high(12) | +// variant(2) | clock_seq(14) | node(48) +// v6: time_high(32) | time_mid(16) | version(4) | time_low(12) | +// variant(2) | clock_seq(14) | node(48) +// v7: unix_ts_ms(48) | version(4) | rand_a(12) | variant(2) | rand_b(62) // -// Note that the time attributes are only relevant to versions 1, 6 and 7. +// v1 and v6 encode the same 60-bit timestamp in 100-nanosecond intervals +// since 1582-10-15, with different field ordering. v7 uses milliseconds +// since the Unix epoch. Its rand_a and rand_b fields hold random/counter +// bits, with no clock sequence or node field. // -// Version field is located in byte 6; most significant 4 bits: +// M is the version field: the most significant 4 bits of byte 6. // -// Variant field is located in byte 8; most significant variable bits: +// N contains the variant: the most significant variable bits of byte 8. // 0xxx: Reserved for NCS compatibility // 10xx: RFC 4122/9562 (standard) // 110x: Reserved for Microsoft compatibility From 9d693e5efa7575455f12ab1181083c61e8549abb Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Mon, 19 Oct 2026 17:34:36 -0700 Subject: [PATCH 24/38] gh-139122: Apply UUID review cleanup --- Modules/_uuidmodule.c | 308 +++++++++++++++++++----------------------- 1 file changed, 140 insertions(+), 168 deletions(-) diff --git a/Modules/_uuidmodule.c b/Modules/_uuidmodule.c index 5e6d67d4edc6945..912b173f5af52aa 100644 --- a/Modules/_uuidmodule.c +++ b/Modules/_uuidmodule.c @@ -13,15 +13,11 @@ # define Py_BUILD_CORE_MODULE 1 #endif -#include "pyconfig.h" // Py_GIL_DISABLED #include "Python.h" -#include // for strncasecmp -#include "structmember.h" // for PyMemberDef #include "pycore_critical_section.h" // _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED() -#include "pycore_long.h" // _PyLong_FromByteArray, _PyLong_AsByteArray +#include "pycore_long.h" // _PyLong_AsByteArray(), _PyLong_GetOne() #include "pycore_pylifecycle.h" // _PyOS_URandom() -#include "pycore_time.h" // PyTime_Time #include "pycore_runtime_init.h" // _Py_ID() #if defined(HAVE_UUID_H) @@ -49,6 +45,8 @@ #endif #endif +#include // memcpy(), memset(), strncmp() + #ifndef MS_WINDOWS @@ -125,14 +123,18 @@ py_windows_has_stable_node(void) #endif /* MS_WINDOWS */ +#define UUID_SIZE 16 + typedef struct uuidobject { PyObject_HEAD - uint8_t bytes[16]; + uint8_t bytes[UUID_SIZE]; Py_hash_t cached_hash; PyObject *is_safe; PyObject *weakreflist; } uuidobject; +#define uuidobject_CAST(OP) ((uuidobject *)(OP)) + // UUID Structure per RFC 9562: // @@ -167,7 +169,7 @@ typedef struct uuidobject { /* State of the _uuid module */ typedef struct { - PyTypeObject *UuidType; + PyTypeObject *uuid_type; PyObject *safe_uuid; @@ -179,7 +181,7 @@ typedef struct { // A freelist for uuid objects -- 15-20% performance boost. uuidobject *freelist; - uint64_t freelist_size; + size_t freelist_size; // We overfetch entropy to speed up successive uuid generations; // this enables 10x peformance boost. @@ -196,16 +198,16 @@ typedef struct { #include "clinic/_uuidmodule.c.h" /*[clinic input] -class _uuid.UUID "uuidobject *" "&UuidType" +class _uuid.UUID "uuidobject *" "&uuid_type" [clinic start generated code]*/ -/*[clinic end generated code: output=da39a3ee5e6b4b0d input=84ae6e2089cffd3f]*/ +/*[clinic end generated code: output=da39a3ee5e6b4b0d input=b62c7d736c192881]*/ // Forward declarations static int from_hex(uuidobject *self, PyObject *hex); static int from_bytes_le(uuidobject *self, PyObject *bytes_le); static int from_int(uuidobject *self, PyObject *int_value, int validate); static int from_fields(uuidobject *self, PyObject *fields); -static PyObject * get_SafeUUID(uuid_state *state); +static PyObject *get_SafeUUID(uuid_state *state); static uint64_t uuid_getpid(void) @@ -238,7 +240,7 @@ get_uuid_state_by_cls(PyTypeObject *cls) } // Forward declaration -static PyObject *uuid_from_bytes_array(PyTypeObject *type, uint8_t bytes[16]); +static PyObject *uuid_from_bytes_array(PyTypeObject *type, uint8_t bytes[UUID_SIZE]); static int gen_time(uuid_state *state, PyTime_t *time) @@ -353,9 +355,9 @@ _uuid_uuid4_impl(PyObject *module) /*[clinic end generated code: output=b835af30d9d6efc5 input=9cfb3a3b71c25cdf]*/ { uuid_state *state = get_uuid_state(module); - uint8_t bytes[16]; + uint8_t bytes[UUID_SIZE]; - if (gen_random_lock_held(module, state, bytes, 16) < 0) { + if (gen_random_lock_held(module, state, bytes, sizeof(bytes)) < 0) { return NULL; } @@ -363,7 +365,7 @@ _uuid_uuid4_impl(PyObject *module) bytes[6] = (uint8_t)((bytes[6] & 0x0f) | 0x40); bytes[8] = (uint8_t)((bytes[8] & 0x3f) | 0x80); - return uuid_from_bytes_array(state->UuidType, bytes); + return uuid_from_bytes_array(state->uuid_type, bytes); } static inline int @@ -371,7 +373,7 @@ uuid7_get_counter_and_tail(PyObject *module, uuid_state *state, uint64_t *counter, uint8_t *tail) { uint8_t rand_bytes[10]; - if (gen_random_lock_held(module, state, rand_bytes, 10) < 0) { + if (gen_random_lock_held(module, state, rand_bytes, sizeof(rand_bytes)) < 0) { return -1; } @@ -404,7 +406,7 @@ _uuid_uuid7_impl(PyObject *module) /*[clinic end generated code: output=f301accc11162c91 input=88514d61dc785108]*/ { uuid_state *state = get_uuid_state(module); - uint8_t bytes[16]; + uint8_t bytes[UUID_SIZE]; uint64_t timestamp_ms, counter; PyTime_t pytime; @@ -417,7 +419,8 @@ _uuid_uuid7_impl(PyObject *module) if (uuid7_get_counter_and_tail(module, state, &counter, bytes + 12) < 0) { return NULL; } - } else { + } + else { if (timestamp_ms < state->last_timestamp_v7) { timestamp_ms = state->last_timestamp_v7 + 1; } @@ -429,7 +432,8 @@ _uuid_uuid7_impl(PyObject *module) if (uuid7_get_counter_and_tail(module, state, &counter, bytes + 12) < 0) { return NULL; } - } else { + } + else { // This is the common fast path, we only need 4 bytes of entropy // 32-bit random data if (gen_random_lock_held(module, state, bytes + 12, 4) < 0) { @@ -462,7 +466,7 @@ _uuid_uuid7_impl(PyObject *module) state->last_timestamp_v7 = timestamp_ms; state->last_counter_v7 = counter; - return uuid_from_bytes_array(state->UuidType, bytes); + return uuid_from_bytes_array(state->uuid_type, bytes); } /*[clinic input] @@ -553,14 +557,14 @@ _uuid_UUID___init___impl(uuidobject *self, PyObject *hex, PyObject *bytes, PyErr_SetString(PyExc_TypeError, "bytes must be a bytes object"); return -1; } - if (PyBytes_GET_SIZE(bytes) != 16) { + if (PyBytes_GET_SIZE(bytes) != UUID_SIZE) { PyErr_SetString( PyExc_ValueError, "bytes is not a 16-char string" ); return -1; } - memcpy(self->bytes, PyBytes_AS_STRING(bytes), 16); + memcpy(self->bytes, PyBytes_AS_STRING(bytes), sizeof(self->bytes)); } else if (bytes_le != Py_None) { if (from_bytes_le(self, bytes_le) < 0) { @@ -709,7 +713,7 @@ from_hex(uuidobject *self, PyObject *hex) acc_set = 1; } - if (j > 16 || (j == 16 && acc_set)) { + if (j > UUID_SIZE || (j == UUID_SIZE && acc_set)) { PyErr_Format(PyExc_ValueError, "invalid UUID '%s': decodes to more than 16 bytes", hex); @@ -717,7 +721,7 @@ from_hex(uuidobject *self, PyObject *hex) } } - if (j != 16) { + if (j != UUID_SIZE) { PyErr_Format(PyExc_ValueError, "invalid UUID '%s': decodes to less than 16 bytes", hex); @@ -734,7 +738,7 @@ from_bytes_le(uuidobject *self, PyObject *bytes_le) PyErr_SetString(PyExc_TypeError, "bytes_le must be a bytes object"); return -1; } - if (PyBytes_GET_SIZE(bytes_le) != 16) { + if (PyBytes_GET_SIZE(bytes_le) != UUID_SIZE) { PyErr_SetString(PyExc_ValueError, "bytes_le is not a 16-char string"); return -1; @@ -815,19 +819,14 @@ from_int(uuidobject *self, PyObject *int_value, int validate) return -1; } - if (_PyLong_AsByteArray( - (PyLongObject *)int_value, - (unsigned char *)self->bytes, - 16, - 0, // big-endian - 0, // unsigned - 1 // with_exceptions - ) < 0) - { - return -1; - } - - return 0; + return _PyLong_AsByteArray( + (PyLongObject *)int_value, + (unsigned char *)self->bytes, + sizeof(self->bytes), + 0, // big-endian + 0, // unsigned + 1 // with_exceptions + ); } static int @@ -880,16 +879,17 @@ from_fields(uuidobject *self, PyObject *fields) return -1; } -# define EXTRACT_FIELD(field_num, max_value, error_msg, type, name) \ - type name; \ - do { \ - uint64_t name##_extracted; \ - if (extract_field(fields, field_num, max_value, error_msg, \ - &(name##_extracted)) < 0) { \ - return -1; \ - } \ - name = (type)name##_extracted; \ - } while(0) +#define EXTRACT_FIELD(FIELD_NUM, MAX_VALUE, ERROR_MSG, TYPE, NAME) \ + TYPE NAME; \ + do { \ + uint64_t NAME##_extracted; \ + if (extract_field(fields, FIELD_NUM, MAX_VALUE, ERROR_MSG, \ + &NAME##_extracted) < 0) \ + { \ + return -1; \ + } \ + NAME = (TYPE)NAME##_extracted; \ + } while (0) EXTRACT_FIELD( 0, (1ULL << 32) - 1, "field 1 out of range (need a 32-bit value)", @@ -916,6 +916,8 @@ from_fields(uuidobject *self, PyObject *fields) uint64_t, node ); +#undef EXTRACT_FIELD + self->bytes[0] = (uint8_t)(time_low >> 24); self->bytes[1] = (uint8_t)(time_low >> 16); self->bytes[2] = (uint8_t)(time_low >> 8); @@ -944,20 +946,14 @@ from_fields(uuidobject *self, PyObject *fields) static PyObject * get_int(uuidobject *self) { - return _PyLong_FromByteArray((unsigned char *)self->bytes, 16, 0, 0); + return _PyLong_FromByteArray( + (unsigned char *)self->bytes, sizeof(self->bytes), 0, 0); } static PyObject * -get_uuidmod_attr(uuid_state *state, const char *name) +get_uuidmod_attr(const char *name) { - PyObject *mod = PyImport_ImportModule("uuid"); - if (mod == NULL) { - return NULL; - } - - PyObject *ret = PyObject_GetAttrString(mod, name); - Py_DECREF(mod); - return ret; + return PyImport_ImportModuleAttrString("uuid", name); } static PyObject * @@ -967,11 +963,8 @@ get_SafeUUID(uuid_state *state) return Py_NewRef(state->safe_uuid); } - state->safe_uuid = get_uuidmod_attr(state, "SafeUUID"); - if (state->safe_uuid == NULL) { - return NULL; - } - return Py_NewRef(state->safe_uuid); + state->safe_uuid = get_uuidmod_attr("SafeUUID"); + return Py_XNewRef(state->safe_uuid); } static uuidobject * @@ -980,9 +973,9 @@ make_uuid(PyTypeObject *type) uuidobject *self = NULL; uuid_state *state = get_uuid_state_by_cls(type); - if (type != state->UuidType) { + if (type != state->uuid_type) { // Allocate the subclass's full instance size and enable GC tracking. - self = (uuidobject *)PyType_GenericAlloc(type, 0); + self = uuidobject_CAST(PyType_GenericAlloc(type, 0)); if (self == NULL) { return NULL; } @@ -992,7 +985,7 @@ make_uuid(PyTypeObject *type) Py_BEGIN_CRITICAL_SECTION(type); if (state->freelist_size > 0) { self = state->freelist; - state->freelist = (uuidobject *)self->weakreflist; + state->freelist = uuidobject_CAST(self->weakreflist); state->freelist_size--; } Py_END_CRITICAL_SECTION(); @@ -1026,7 +1019,7 @@ Uuid_alloc(PyTypeObject *type, Py_ssize_t Py_UNUSED(nitems)) if (self == NULL) { return NULL; } - memset(self->bytes, 0, 16); + memset(self->bytes, 0, sizeof(self->bytes)); return (PyObject *)self; } @@ -1036,14 +1029,14 @@ Uuid_dealloc(PyObject *obj) PyTypeObject *type = Py_TYPE(obj); uuid_state *state = get_uuid_state_by_cls(type); - uuidobject *uuid = (uuidobject *)obj; + uuidobject *uuid = uuidobject_CAST(obj); if (uuid->weakreflist != NULL) { PyObject_ClearWeakRefs(obj); } Py_CLEAR(uuid->is_safe); int skip_dealloc = 0; - if (type != state->UuidType) { + if (type != state->uuid_type) { // We only apply the freelist optimization to the known C type, // not any subclasses of it. goto dealloc; @@ -1070,15 +1063,15 @@ Uuid_dealloc(PyObject *obj) static PyObject * -Uuid_get_int(PyObject *o, void *closure) +Uuid_get_int(PyObject *op, void *Py_UNUSED(closure)) { - return get_int((uuidobject *)o); + return get_int(uuidobject_CAST(op)); } static PyObject * -Uuid_get_is_safe(PyObject *o, void *closure) +Uuid_get_is_safe(PyObject *op, void *Py_UNUSED(closure)) { - uuidobject *self = (uuidobject *)o; + uuidobject *self = uuidobject_CAST(op); uuid_state *state = get_uuid_state_by_cls(Py_TYPE(self)); if (self->is_safe == NULL) { PyObject *SafeUUID = get_SafeUUID(state); @@ -1090,44 +1083,42 @@ Uuid_get_is_safe(PyObject *o, void *closure) } static PyObject * -Uuid_get_hex(PyObject *o, void *closure) +Uuid_get_hex(PyObject *op, void *Py_UNUSED(closure)) { - uuidobject *self = (uuidobject *)o; - char hex[32]; - for (int i = 0; i < 16; i++) { + uuidobject *self = uuidobject_CAST(op); + char hex[UUID_SIZE * 2]; + for (int i = 0; i < UUID_SIZE; i++) { byte_to_hex(self->bytes[i], &hex[i * 2]); } - return PyUnicode_FromStringAndSize(hex, 32); + return PyUnicode_FromStringAndSize(hex, sizeof(hex)); } static PyObject * -Uuid_get_variant(PyObject *o, void *closure) +Uuid_get_variant(PyObject *op, void *Py_UNUSED(closure)) { - uuidobject *self = (uuidobject *)o; - uuid_state *state = get_uuid_state_by_cls(Py_TYPE(self)); - + uuidobject *self = uuidobject_CAST(op); uint8_t variant_byte = self->bytes[8]; // xxx - three high bits of variant_byte are unknown if (!(variant_byte & 0x80)) { // & 0b1000_0000 // 0xx - RESERVED_NCS - return get_uuidmod_attr(state, "RESERVED_NCS"); + return get_uuidmod_attr("RESERVED_NCS"); } // 1xx -- we know that high bit must be 1 if (!(variant_byte & 0x40)) { // & 0b0100_0000 // 10x - RFC_4122 - return get_uuidmod_attr(state, "RFC_4122"); + return get_uuidmod_attr("RFC_4122"); } // 11x -- we know that two high bits are 1 if (!(variant_byte & 0x20)) { // & 0b0010_0000 // 110 - RESERVED_MICROSOFT - return get_uuidmod_attr(state, "RESERVED_MICROSOFT"); + return get_uuidmod_attr("RESERVED_MICROSOFT"); } // 111 -- we know that all three high bits are 1 - RESERVED_FUTURE - return get_uuidmod_attr(state, "RESERVED_FUTURE"); + return get_uuidmod_attr("RESERVED_FUTURE"); } static int @@ -1149,9 +1140,9 @@ get_version(uuidobject *self) } static PyObject * -Uuid_get_version(PyObject *o, void *closure) +Uuid_get_version(PyObject *op, void *Py_UNUSED(closure)) { - uuidobject *self = (uuidobject *)o; + uuidobject *self = uuidobject_CAST(op); if (!is_rfc_4122(self)) { Py_RETURN_NONE; } @@ -1203,44 +1194,44 @@ get_node(uuidobject *self) } static PyObject * -Uuid_get_time_low(PyObject *o, void *closure) +Uuid_get_time_low(PyObject *op, void *Py_UNUSED(closure)) { - uuidobject *self = (uuidobject *)o; + uuidobject *self = uuidobject_CAST(op); return PyLong_FromUnsignedLong(get_time_low(self)); } static PyObject * -Uuid_get_time_mid(PyObject *o, void *closure) +Uuid_get_time_mid(PyObject *op, void *Py_UNUSED(closure)) { - uuidobject *self = (uuidobject *)o; + uuidobject *self = uuidobject_CAST(op); return PyLong_FromUnsignedLong(get_time_mid(self)); } static PyObject * -Uuid_get_time_hi_version(PyObject *o, void *closure) +Uuid_get_time_hi_version(PyObject *op, void *Py_UNUSED(closure)) { - uuidobject *self = (uuidobject *)o; + uuidobject *self = uuidobject_CAST(op); return PyLong_FromUnsignedLong(get_time_hi_version(self)); } static PyObject * -Uuid_get_clock_seq_hi_variant(PyObject *o, void *closure) +Uuid_get_clock_seq_hi_variant(PyObject *op, void *Py_UNUSED(closure)) { - uuidobject *self = (uuidobject *)o; + uuidobject *self = uuidobject_CAST(op); return PyLong_FromUnsignedLong(get_clock_seq_hi_variant(self)); } static PyObject * -Uuid_get_clock_seq_low(PyObject *o, void *closure) +Uuid_get_clock_seq_low(PyObject *op, void *Py_UNUSED(closure)) { - uuidobject *self = (uuidobject *)o; + uuidobject *self = uuidobject_CAST(op); return PyLong_FromUnsignedLong(get_clock_seq_low(self)); } static PyObject * -Uuid_get_time(PyObject *o, void *closure) +Uuid_get_time(PyObject *op, void *Py_UNUSED(closure)) { - uuidobject *self = (uuidobject *)o; + uuidobject *self = uuidobject_CAST(op); long version = get_version(self); @@ -1283,9 +1274,9 @@ Uuid_get_time(PyObject *o, void *closure) } static PyObject * -Uuid_get_clock_seq(PyObject *o, void *closure) +Uuid_get_clock_seq(PyObject *op, void *Py_UNUSED(closure)) { - uuidobject *self = (uuidobject *)o; + uuidobject *self = uuidobject_CAST(op); // clock_seq_hi_variant (byte 8) & 0x3f, then clock_seq_low (byte 9) uint16_t clock_seq = (uint16_t)(((get_clock_seq_hi_variant(self) & 0x3f) << 8) | get_clock_seq_low(self)); @@ -1293,30 +1284,30 @@ Uuid_get_clock_seq(PyObject *o, void *closure) } static PyObject * -Uuid_get_node(PyObject *o, void *closure) +Uuid_get_node(PyObject *op, void *Py_UNUSED(closure)) { - uuidobject *self = (uuidobject *)o; + uuidobject *self = uuidobject_CAST(op); return PyLong_FromUnsignedLongLong(get_node(self)); } static PyObject * -Uuid_get_bytes(PyObject *o, void *closure) +Uuid_get_bytes(PyObject *op, void *Py_UNUSED(closure)) { - uuidobject *self = (uuidobject *)o; - return PyBytes_FromStringAndSize((const char *)self->bytes, 16); + uuidobject *self = uuidobject_CAST(op); + return PyBytes_FromStringAndSize((const char *)self->bytes, sizeof(self->bytes)); } static PyObject * -Uuid_get_bytes_le(PyObject *o, void *closure) +Uuid_get_bytes_le(PyObject *op, void *Py_UNUSED(closure)) { - uuidobject *self = (uuidobject *)o; + uuidobject *self = uuidobject_CAST(op); // UUID fields in little-endian order need to be byte-swapped: // - time_low (4 bytes) - reversed // - time_mid (2 bytes) - reversed // - time_hi_version (2 bytes) - reversed // - clock_seq and node (8 bytes) - unchanged - unsigned char bytes_le[16]; + unsigned char bytes_le[UUID_SIZE]; // Reverse time_low (bytes 0-3) bytes_le[0] = self->bytes[3]; @@ -1335,13 +1326,13 @@ Uuid_get_bytes_le(PyObject *o, void *closure) // Copy clock_seq and node as-is (bytes 8-15) memcpy(bytes_le + 8, self->bytes + 8, 8); - return PyBytes_FromStringAndSize((const char *)bytes_le, 16); + return PyBytes_FromStringAndSize((const char *)bytes_le, sizeof(bytes_le)); } static PyObject * -Uuid_get_fields(PyObject *o, void *closure) +Uuid_get_fields(PyObject *op, void *Py_UNUSED(closure)) { - uuidobject *self = (uuidobject *)o; + uuidobject *self = uuidobject_CAST(op); uint32_t time_low = get_time_low(self); uint16_t time_mid = get_time_mid(self); uint16_t time_hi_version = get_time_hi_version(self); @@ -1362,20 +1353,20 @@ Uuid_get_fields(PyObject *o, void *closure) } static PyObject * -uuid_from_bytes_array(PyTypeObject *type, uint8_t bytes[16]) +uuid_from_bytes_array(PyTypeObject *type, uint8_t bytes[UUID_SIZE]) { uuidobject *self = make_uuid(type); if (self == NULL) { return NULL; } - memcpy(self->bytes, bytes, 16); + memcpy(self->bytes, bytes, sizeof(self->bytes)); return (PyObject *)self; } static PyObject * Uuid_nb_int(PyObject *self) { - return get_int((uuidobject *)self); + return get_int(uuidobject_CAST(self)); } static PyObject * @@ -1383,14 +1374,14 @@ Uuid_richcompare(PyObject *self, PyObject *other, int op) { uuid_state *state = get_uuid_state_by_cls(Py_TYPE(self)); - if (!PyObject_TypeCheck(other, state->UuidType)) { + if (!PyObject_TypeCheck(other, state->uuid_type)) { Py_RETURN_NOTIMPLEMENTED; } - uuidobject *uuid_self = (uuidobject *)self; - uuidobject *uuid_other = (uuidobject *)other; + uuidobject *uuid_self = uuidobject_CAST(self); + uuidobject *uuid_other = uuidobject_CAST(other); - int cmp = memcmp(uuid_self->bytes, uuid_other->bytes, 16); + int cmp = memcmp(uuid_self->bytes, uuid_other->bytes, sizeof(uuid_self->bytes)); int result; switch (op) { @@ -1418,7 +1409,8 @@ Uuid_richcompare(PyObject *self, PyObject *other, int op) if (result) { Py_RETURN_TRUE; - } else { + } + else { Py_RETURN_FALSE; } } @@ -1426,7 +1418,7 @@ Uuid_richcompare(PyObject *self, PyObject *other, int op) static PyObject * Uuid_str(PyObject *self) { - uuidobject *uuid = (uuidobject *)self; + uuidobject *uuid = uuidobject_CAST(self); // UUID string format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (36 chars) char str[36]; @@ -1451,11 +1443,11 @@ Uuid_str(PyObject *self) } str[23] = '-'; - for (int i = 10; i < 16; i++) { + for (int i = 10; i < UUID_SIZE; i++) { byte_to_hex(uuid->bytes[i], &str[24 + (i - 10) * 2]); } - return PyUnicode_FromStringAndSize(str, 36); + return PyUnicode_FromStringAndSize(str, sizeof(str)); } static PyObject * @@ -1480,17 +1472,17 @@ Uuid_repr(PyObject *self) } static int -Uuid_setattr(PyObject *self, PyObject *name, PyObject *value) +Uuid_setattr(PyObject *Py_UNUSED(self), PyObject *Py_UNUSED(name), + PyObject *Py_UNUSED(value)) { PyErr_SetString(PyExc_TypeError, "UUID objects are immutable"); return -1; } static PyObject * -Uuid_get_urn(PyObject *o, void *closure) +Uuid_get_urn(PyObject *op, void *Py_UNUSED(closure)) { - uuidobject *self = (uuidobject *)o; - PyObject *str_obj = Uuid_str((PyObject *)self); + PyObject *str_obj = Uuid_str(op); if (str_obj == NULL) { return NULL; } @@ -1501,18 +1493,15 @@ Uuid_get_urn(PyObject *o, void *closure) } static Py_hash_t -Uuid_hash(PyObject *o) +Uuid_hash(PyObject *op) { - uuidobject *uuid = (uuidobject *)o; - if (uuid->cached_hash != -1) { - // UUIDs are very often used in dicts/sets, so it makes - // sense to cache the computed hash (like we do for str) - return uuid->cached_hash; + uuidobject *uuid = uuidobject_CAST(op); + // UUIDs are very often used in dicts/sets, so it makes + // sense to cache the computed hash (like we do for str). + if (uuid->cached_hash == -1) { + uuid->cached_hash = Py_HashBuffer(uuid->bytes, sizeof(uuid->bytes)); } - - Py_hash_t hash = Py_HashBuffer(uuid->bytes, 16); - uuid->cached_hash = hash; - return hash; + return uuid->cached_hash; } @@ -1672,37 +1661,20 @@ compute_uuid_max(void) { // Compute `(1 << 128) - 1` - PyObject *one = NULL; - PyObject *shift = NULL; - PyObject *shifted = NULL; - - one = PyLong_FromLong(1); - if (one == NULL) { - goto err; - } - - shift = PyLong_FromLong(128); + PyObject *shift = PyLong_FromLong(128); if (shift == NULL) { - goto err; + return NULL; } - shifted = PyNumber_Lshift(one, shift); + PyObject *shifted = PyNumber_Lshift(_PyLong_GetOne(), shift); + Py_DECREF(shift); if (shifted == NULL) { - goto err; + return NULL; } - Py_DECREF(shift); - PyObject *result = PyNumber_Subtract(shifted, one); + PyObject *result = PyNumber_Subtract(shifted, _PyLong_GetOne()); Py_DECREF(shifted); - Py_DECREF(one); - return result; - -err: - Py_XDECREF(one); - Py_XDECREF(shift); - Py_XDECREF(shifted); - return NULL; } static PyMethodDef Uuid_methods[] = { @@ -1759,7 +1731,7 @@ static int module_traverse(PyObject *mod, visitproc visit, void *arg) { uuid_state *state = get_uuid_state(mod); - Py_VISIT(state->UuidType); + Py_VISIT(state->uuid_type); Py_VISIT(state->safe_uuid); Py_VISIT(state->uint128_max); Py_VISIT(state->random_func); @@ -1773,7 +1745,7 @@ module_clear(PyObject *mod) { uuid_state *state = get_uuid_state(mod); - Py_CLEAR(state->UuidType); + Py_CLEAR(state->uuid_type); Py_CLEAR(state->safe_uuid); Py_CLEAR(state->uint128_max); Py_CLEAR(state->random_func); @@ -1783,7 +1755,7 @@ module_clear(PyObject *mod) if (state->freelist != NULL) { while (state->freelist != NULL) { uuidobject *cur = state->freelist; - state->freelist = (uuidobject *)cur->weakreflist; + state->freelist = uuidobject_CAST(cur->weakreflist); PyObject_Free(cur); } state->freelist = NULL; @@ -1812,7 +1784,7 @@ uuid_exec(PyObject *module) } \ } while (0) - assert(sizeof(uuid_t) == 16); + assert(sizeof(uuid_t) == UUID_SIZE); #if defined(MS_WINDOWS) ADD_INT("has_uuid_generate_time_safe", 0); #elif defined(HAVE_UUID_GENERATE_TIME_SAFE) @@ -1831,16 +1803,16 @@ uuid_exec(PyObject *module) #undef ADD_INT - state->UuidType = (PyTypeObject *)PyType_FromMetaclass( + state->uuid_type = (PyTypeObject *)PyType_FromMetaclass( NULL, module, &Uuid_spec, NULL ); - if (state->UuidType == NULL) { + if (state->uuid_type == NULL) { return -1; } - if (PyModule_AddType(module, state->UuidType) < 0) { + if (PyModule_AddType(module, state->uuid_type) < 0) { return -1; } From 511783c705f92dfb4fb154947abf0bc4886d3d8b Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Tue, 20 Oct 2026 13:56:54 -0700 Subject: [PATCH 25/38] gh-139122: Fix concurrent UUID entropy refills and UUIDv7 state updates --- Lib/test/test_uuid.py | 118 +++++++++++++++++++++++++++++++++++++++++- Modules/_uuidmodule.c | 82 ++++++++++++++++++----------- 2 files changed, 169 insertions(+), 31 deletions(-) diff --git a/Lib/test/test_uuid.py b/Lib/test/test_uuid.py index 5aac4a2f5e58101..ce88f8c788b523c 100755 --- a/Lib/test/test_uuid.py +++ b/Lib/test/test_uuid.py @@ -16,7 +16,10 @@ from unittest import mock from test import support -from test.support import force_not_colorized_test_class, import_helper, warnings_helper +from test.support import ( + force_not_colorized_test_class, import_helper, threading_helper, + warnings_helper, +) from test.support.script_helper import assert_python_ok py_uuid = import_helper.import_fresh_module('uuid', blocked=['_uuid']) @@ -1860,6 +1863,119 @@ def test_exact_same_algo_uuid4(self): finally: self._reset_hooks(uuid) + @threading_helper.requires_working_threading() + @threading_helper.reap_threads + def test_concurrent_entropy_refill(self): + # Concurrent calls must not consume the same cached entropy. Pause one + # UUIDv4 call during a refill after it consumes a partial cached block, + # then run another call. Each thread supplies its ID as entropy, making + # it possible to detect reuse of the first thread's cached prefix. + from concurrent.futures import ThreadPoolExecutor + import threading + import uuid + + entered = threading.Event() + resume = threading.Event() + local = threading.local() + local.id = 1 + local.pause_refill = False + + def random_func(size): + local.refill_size = size + if local.pause_refill: + entered.set() + if not resume.wait(support.SHORT_TIMEOUT): + raise TimeoutError('entropy refill was not resumed') + return bytes([local.id]) * size + + def generate(thread_id): + local.id = thread_id + local.pause_refill = thread_id == 1 + return uuid.uuid4() + + uuid._install_c_hooks(random_func=random_func, time_func=lambda: 0) + self.addCleanup(uuid._install_c_hooks, random_func=None, time_func=None) + + # UUIDv7 consumes 10 bytes initially. Leave less than one UUIDv4's + # entropy cached, so the next call must combine cached and fresh bytes. + uuid.uuid7() + size = len(uuid.UUID(int=0).bytes) + cached = local.refill_size - 10 + for _ in range(cached // size): + uuid.uuid4() + remaining = cached % size + self.assertGreater(remaining, 0) + + with ThreadPoolExecutor(2) as pool: + pending = pool.submit(generate, 1) + try: + self.assertTrue(entered.wait(support.SHORT_TIMEOUT)) + other = pool.submit(generate, 2).result( + timeout=support.SHORT_TIMEOUT) + finally: + resume.set() + first = pending.result(timeout=support.SHORT_TIMEOUT) + + # The waiting call reserved the cached prefix; the other call must + # obtain its prefix from a new entropy block instead of reusing it. + self.assertNotEqual(first.bytes[:remaining], other.bytes[:remaining]) + + @threading_helper.requires_working_threading() + @threading_helper.reap_threads + def test_concurrent_uuid7_state(self): + # UUIDv7 must recheck its counter if a refill lets another call advance + # it. Pause one call in the entropy hook and complete another first; + # with fixed time and entropy, the resumed call needs a higher counter. + # Exercise both the first UUIDv7 call and an already initialized counter. + from concurrent.futures import ThreadPoolExecutor + import threading + import uuid + + for initialized in (False, True): + with self.subTest(initialized=initialized): + entered = threading.Event() + resume = threading.Event() + calls = [] + lock = threading.Lock() + blocked_call = 2 if initialized else 1 + + def random_func(size): + with lock: + calls.append(size) + call = len(calls) + if call == blocked_call: + entered.set() + if not resume.wait(support.SHORT_TIMEOUT): + raise TimeoutError('entropy refill was not resumed') + # Keep time and entropy identical, so only the shared + # counter can distinguish the concurrent UUIDs. + return bytes(size) + + uuid._install_c_hooks(random_func=random_func, + time_func=lambda: 0) + try: + if initialized: + uuid.uuid7() + # Consume the remaining cache in four-byte tails, + # leaving the next UUIDv7 call to trigger a refill. + for _ in range((calls[0] - 10) // 4): + uuid.uuid7() + + with ThreadPoolExecutor(1) as pool: + pending = pool.submit(uuid.uuid7) + try: + self.assertTrue(entered.wait(support.SHORT_TIMEOUT)) + other = uuid.uuid7() + finally: + resume.set() + first = pending.result(timeout=support.SHORT_TIMEOUT) + + # A call resumed after another UUID was generated must + # advance from that UUID's state, not its stale snapshot. + self.assertGreater(first, other) + finally: + uuid._install_c_hooks(random_func=None, time_func=None) + def test_exact_same_algo_uuid7(self): import uuid diff --git a/Modules/_uuidmodule.c b/Modules/_uuidmodule.c index 912b173f5af52aa..c4f7f5b02d6ecb5 100644 --- a/Modules/_uuidmodule.c +++ b/Modules/_uuidmodule.c @@ -249,7 +249,9 @@ gen_time(uuid_state *state, PyTime_t *time) return PyTime_Time(time); } - PyObject *ret = PyObject_CallNoArgs(state->time_func); + PyObject *time_func = Py_NewRef(state->time_func); + PyObject *ret = PyObject_CallNoArgs(time_func); + Py_DECREF(time_func); if (ret == NULL) { return -1; } @@ -284,7 +286,7 @@ gen_random_lock_held(PyObject *module, uuid_state *state, uint8_t *bytes, state->random_idx = RANDOM_BUF_SIZE; } - if (state->random_idx + size < RANDOM_BUF_SIZE) { + if (state->random_idx + size <= RANDOM_BUF_SIZE) { memcpy(bytes, state->random_buf + state->random_idx, size); state->random_idx += size; } @@ -302,9 +304,16 @@ gen_random_lock_held(PyObject *module, uuid_state *state, uint8_t *bytes, size -= partial; } + // Reserve the cached bytes before a refill can release the module's + // critical section. Another call must not consume the same bytes. + state->random_idx = RANDOM_BUF_SIZE; + uint8_t random_buf[RANDOM_BUF_SIZE]; + if (state->random_func != NULL) { + PyObject *random_func = Py_NewRef(state->random_func); PyObject *buf = PyObject_CallOneArg( - state->random_func, state->random_size_int); + random_func, state->random_size_int); + Py_DECREF(random_func); if (buf == NULL) { return -1; } @@ -325,17 +334,20 @@ gen_random_lock_held(PyObject *module, uuid_state *state, uint8_t *bytes, return -1; } - memcpy(state->random_buf, PyBytes_AsString(buf), RANDOM_BUF_SIZE); + memcpy(random_buf, PyBytes_AsString(buf), sizeof(random_buf)); Py_DECREF(buf); } else { - // Pure Python implementation uses os.urandom() which - // wraps _PyOS_URandom - if (_PyOS_URandom(state->random_buf, RANDOM_BUF_SIZE) < 0) { + // _PyOS_URandom() releases the thread state. Fill a private buffer + // so concurrent refills cannot write into the same memory. + if (_PyOS_URandom(random_buf, sizeof(random_buf)) < 0) { return -1; } } + // We hold the caller's module lock again after fetching entropy. + // Replace the cache, discarding any unused bytes from a concurrent refill. + memcpy(state->random_buf, random_buf, sizeof(random_buf)); memcpy(bytes, state->random_buf, size); state->random_idx = size; } @@ -389,9 +401,8 @@ uuid7_get_counter_and_tail(PyObject *module, uuid_state *state, } -// There's code that modifies the module state (emulating global variables -// used in the pure Python implementation.) So we're slapping a critical -// section here to make it easier to reason about the C port of this code. +// The module's critical section protects the cached entropy and UUIDv7 state. +// It may be suspended during entropy fetching or a Python testing hook. /*[clinic input] @critical_section _uuid.uuid7 @@ -413,34 +424,45 @@ _uuid_uuid7_impl(PyObject *module) if (gen_time(state, &pytime) < 0) { return NULL; } - timestamp_ms = (uint64_t)(pytime / 1000000); + uint64_t last_timestamp, last_counter; + uint8_t last_timestamp_init; + do { + last_timestamp = state->last_timestamp_v7; + last_counter = state->last_counter_v7; + last_timestamp_init = state->last_timestamp_v7_init; + timestamp_ms = (uint64_t)(pytime / 1000000); - if (!state->last_timestamp_v7_init || timestamp_ms > state->last_timestamp_v7) { - if (uuid7_get_counter_and_tail(module, state, &counter, bytes + 12) < 0) { - return NULL; - } - } - else { - if (timestamp_ms < state->last_timestamp_v7) { - timestamp_ms = state->last_timestamp_v7 + 1; - } - // advance the 42-bit counter - counter = state->last_counter_v7 + 1; - if (counter > 0x3FFFFFFFFFF) { - // advance the 48-bit timestamp - timestamp_ms += 1; + if (!last_timestamp_init || timestamp_ms > last_timestamp) { if (uuid7_get_counter_and_tail(module, state, &counter, bytes + 12) < 0) { return NULL; } } else { - // This is the common fast path, we only need 4 bytes of entropy - // 32-bit random data - if (gen_random_lock_held(module, state, bytes + 12, 4) < 0) { - return NULL; + if (timestamp_ms < last_timestamp) { + timestamp_ms = last_timestamp + 1; + } + // advance the 42-bit counter + counter = last_counter + 1; + if (counter > 0x3FFFFFFFFFF) { + // advance the 48-bit timestamp + timestamp_ms += 1; + if (uuid7_get_counter_and_tail(module, state, &counter, bytes + 12) < 0) { + return NULL; + } + } + else { + // This is the common fast path, we only need 4 bytes of entropy + // 32-bit random data + if (gen_random_lock_held(module, state, bytes + 12, 4) < 0) { + return NULL; + } } } - } + // A refill can suspend the critical section and let another call + // advance the state. Recompute instead of committing a stale counter. + } while (last_timestamp != state->last_timestamp_v7 || + last_counter != state->last_counter_v7 || + last_timestamp_init != state->last_timestamp_v7_init); timestamp_ms &= 0xFFFFFFFFFFFF; bytes[0] = (uint8_t)(timestamp_ms >> 40); From e1085f0f13f3907ecca3c360e02941b1cff3a2d1 Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Tue, 20 Oct 2026 14:04:51 -0700 Subject: [PATCH 26/38] gh-139122: Avoid subclass module-state lookup during UUID deallocation --- Lib/test/test_uuid.py | 38 ++++++++++++++++++++++++++++++++++++++ Modules/_uuidmodule.c | 10 +++++++--- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_uuid.py b/Lib/test/test_uuid.py index ce88f8c788b523c..53a471e24668eb1 100755 --- a/Lib/test/test_uuid.py +++ b/Lib/test/test_uuid.py @@ -2045,6 +2045,44 @@ class U(uuid.UUID): assert base.int == 2 ''')) + def test_subclass_module_finalization(self): + # GC may clear the base type's module reference before a subclass-owned + # instance is destroyed. Drop both modules and collect the subclass's + # class/instance cycle; deallocation must work without module state. + # Isolate the module removals and a possible crash in a subprocess. + assert_python_ok('-c', textwrap.dedent(''' + import gc + import sys + import weakref + + for from_int in (False, True): + import uuid + + module = sys.modules['_uuid'] + UUID = module.UUID + assert uuid.UUID is UUID + + # Remove the module's UUID constants before creating the cycle. + del sys.modules['uuid'] + del uuid + gc.collect() + + class Sub(UUID): + pass + + if from_int: + Sub.keep = Sub._from_int(1) + else: + Sub.keep = Sub(int=1) + refs = [weakref.ref(obj) for obj in + (module, UUID, Sub, Sub.keep)] + + del sys.modules['_uuid'] + del module, UUID, Sub + gc.collect() + assert all(ref() is None for ref in refs) + ''')) + def test_freelist_type_lifetime(self): # Reused live instances must keep their type and module alive, while # cached instances must allow both to be collected. diff --git a/Modules/_uuidmodule.c b/Modules/_uuidmodule.c index c4f7f5b02d6ecb5..061ec6e40860182 100644 --- a/Modules/_uuidmodule.c +++ b/Modules/_uuidmodule.c @@ -1049,7 +1049,6 @@ static void Uuid_dealloc(PyObject *obj) { PyTypeObject *type = Py_TYPE(obj); - uuid_state *state = get_uuid_state_by_cls(type); uuidobject *uuid = uuidobject_CAST(obj); if (uuid->weakreflist != NULL) { @@ -1058,9 +1057,14 @@ Uuid_dealloc(PyObject *obj) Py_CLEAR(uuid->is_safe); int skip_dealloc = 0; + // Only the base UUID type uses the freelist. Do not search a subclass's + // MRO: GC may already have cleared its base type's module reference. + PyObject *module = PyType_GetModule_DuringGC(type); + if (module == NULL || PyModule_GetDef(module) != &uuidmodule) { + goto dealloc; + } + uuid_state *state = get_uuid_state(module); if (type != state->uuid_type) { - // We only apply the freelist optimization to the known C type, - // not any subclasses of it. goto dealloc; } From 69166cef2cb4e9ac4029d4297c144cee364f9362 Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Tue, 20 Oct 2026 14:09:48 -0700 Subject: [PATCH 27/38] gh-139122: Mark UUID testing hook as an explicit re-export --- Lib/uuid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/uuid.py b/Lib/uuid.py index 5766f05ed232ae4..f8e602b725c97fd 100644 --- a/Lib/uuid.py +++ b/Lib/uuid.py @@ -968,7 +968,7 @@ def uuid8(a=None, b=None, c=None): _py_UUID = UUID try: from _uuid import UUID, uuid4, uuid7 - from _uuid import _install_c_hooks + from _uuid import _install_c_hooks as _install_c_hooks except ImportError: _c_UUID = None _c_uuid4 = None From c4d400d175103135713f97ea265c7fe574b7c44d Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Tue, 20 Oct 2026 14:11:38 -0700 Subject: [PATCH 28/38] =?UTF-8?q?gh-139122:=20Document=20UUID=20performanc?= =?UTF-8?q?e=20improvements=20in=20What=E2=80=99s=20New?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Doc/whatsnew/3.16.rst | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 24e2df53a28a717..a8477ecf1f83c16 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -845,6 +845,18 @@ unicodedata * The Unicode database has been updated to Unicode 18.0.0. +uuid +---- + +* :class:`uuid.UUID`, :func:`uuid.uuid4`, and :func:`uuid.uuid7` now have + C implementations, with a pure Python fallback. + In microbenchmarks, UUIDv4 and UUIDv7 generation are approximately 15 and + 30 times faster, respectively, than the pure Python implementations. + UUID construction and string conversion are also faster; see + :ref:`whatsnew316-uuid-optimizations` for the benchmark results. + (Contributed by Yury Selivanov in :gh:`139123`.) + + xml --- @@ -953,6 +965,28 @@ re :gh:`152056`.) +.. _whatsnew316-uuid-optimizations: + +uuid +---- + +* C implementations accelerate UUID generation, construction, and string + conversion. Microbenchmarks comparing the C and pure Python implementations + measured the following speedups: + + ================================ ======= + Operation Speedup + ================================ ======= + :func:`uuid.uuid4` generation 15.01x + :func:`uuid.uuid7` generation 29.64x + :class:`uuid.UUID` from a string 6.76x + :class:`uuid.UUID` from bytes 5.16x + :func:`str` conversion of a UUID 6.66x + ================================ ======= + + (Contributed by Yury Selivanov in :gh:`139123`.) + + Removed ======== From fe8c9474ad89fe185c5c05c7ff3074aea0377935 Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Wed, 21 Oct 2026 11:42:46 -0700 Subject: [PATCH 29/38] gh-139122: Match Python UUID prefix and brace parsing --- Lib/test/test_uuid.py | 26 ++++++++++++++++++++++++++ Modules/_uuidmodule.c | 7 ++++--- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_uuid.py b/Lib/test/test_uuid.py index 53a471e24668eb1..1398f42e22071e8 100755 --- a/Lib/test/test_uuid.py +++ b/Lib/test/test_uuid.py @@ -277,6 +277,32 @@ def test_UUID(self): resorted.sort() equal(ascending, resorted) + def test_UUID_prefixes(self): + # Python removes both URN components regardless of their order. Check + # that both implementations accept them with each UUID string format. + value = '12345678-1234-5678-1234-567812345678' + expected = self.uuid.UUID(value) + for prefix in ('', 'urn:', 'uuid:', 'urn:uuid:', 'uuid:urn:'): + for text in (value, value.replace('-', ''), '{' + value + '}'): + with self.subTest(prefix=prefix, text=text): + self.assertEqual(self.uuid.UUID(prefix + text), expected) + + def test_UUID_strip_braces(self): + # str.strip('{}') removes runs of either brace at both ends, without + # requiring matching pairs. Keep the C parser's behavior identical. + value = '12345678-1234-5678-1234-567812345678' + expected = self.uuid.UUID(value) + for leading in ('', '{', '}', '{{{', '}}}', '{}}{'): + for trailing in ('', '{', '}', '{{{', '}}}', '{}}{'): + with self.subTest(leading=leading, trailing=trailing): + self.assertEqual( + self.uuid.UUID(leading + value + trailing), expected) + # Braces within the UUID and inputs containing only braces are invalid. + for text in ('{{}}', '{}{}', value[:8] + '{}' + value[8:]): + with self.subTest(text=text): + with self.assertRaises(ValueError): + self.uuid.UUID(text) + def test_exceptions(self): badvalue = lambda f: self.assertRaises(ValueError, f) badtype = lambda f: self.assertRaises(TypeError, f) diff --git a/Modules/_uuidmodule.c b/Modules/_uuidmodule.c index 061ec6e40860182..3bd09afe5b7711a 100644 --- a/Modules/_uuidmodule.c +++ b/Modules/_uuidmodule.c @@ -674,7 +674,8 @@ from_hex(uuidobject *self, PyObject *hex) // Reimplement `hex = hex.replace('urn:', '').replace('uuid:', '')` if (size > 0 && start[0] == 'u') { - if (size >= 9 && strncmp(start, "urn:uuid:", 9) == 0) { + if (size >= 9 && (strncmp(start, "urn:uuid:", 9) == 0 || + strncmp(start, "uuid:urn:", 9) == 0)) { start += 9; size -= 9; } @@ -689,11 +690,11 @@ from_hex(uuidobject *self, PyObject *hex) } // Reimplement `hex = hex.strip('{}')` - if (size >= 1 && start[0] == '{') { + while (size >= 1 && (start[0] == '{' || start[0] == '}')) { start++; size -= 1; } - if (size >= 1 && start[size - 1] == '}') { + while (size >= 1 && (start[size - 1] == '{' || start[size - 1] == '}')) { size -= 1; } From b6d7ff2d5b77c44a0387a272f6e655b1c43fe1ab Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Wed, 21 Oct 2026 11:45:47 -0700 Subject: [PATCH 30/38] gh-139122: Assert UUID type association is a module during deallocation --- Modules/_uuidmodule.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Modules/_uuidmodule.c b/Modules/_uuidmodule.c index 3bd09afe5b7711a..b04c39b59aac203 100644 --- a/Modules/_uuidmodule.c +++ b/Modules/_uuidmodule.c @@ -1061,7 +1061,11 @@ Uuid_dealloc(PyObject *obj) // Only the base UUID type uses the freelist. Do not search a subclass's // MRO: GC may already have cleared its base type's module reference. PyObject *module = PyType_GetModule_DuringGC(type); - if (module == NULL || PyModule_GetDef(module) != &uuidmodule) { + if (module == NULL) { + goto dealloc; + } + assert(PyModule_Check(module)); + if (PyModule_GetDef(module) != &uuidmodule) { goto dealloc; } uuid_state *state = get_uuid_state(module); From 72673532d43521923b1b44a7a1718aaf40657083 Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Wed, 21 Oct 2026 11:48:17 -0700 Subject: [PATCH 31/38] gh-139122: Propagate SafeUUID lookup errors in is_safe getter --- Lib/test/test_uuid.py | 22 ++++++++++++++++++++++ Modules/_uuidmodule.c | 3 +++ 2 files changed, 25 insertions(+) diff --git a/Lib/test/test_uuid.py b/Lib/test/test_uuid.py index 1398f42e22071e8..e1f017c1138a9bf 100755 --- a/Lib/test/test_uuid.py +++ b/Lib/test/test_uuid.py @@ -1753,6 +1753,28 @@ def time_func(self): @unittest.skipUnless(c_uuid, "requires the C _uuid module") class TestCImplementationCompat(unittest.TestCase): + def test_is_safe_lookup_error(self): + # Force the lazy SafeUUID lookup to fail before it is cached. The getter + # must propagate AttributeError and recover when the attribute returns. + # Run in a subprocess because using the failed lookup can crash Python. + assert_python_ok('-c', textwrap.dedent(''' + import uuid + + obj = uuid.UUID(int=1) + SafeUUID = uuid.SafeUUID + del uuid.SafeUUID + try: + try: + obj.is_safe + except AttributeError as exc: + assert 'SafeUUID' in str(exc) + else: + raise AssertionError('SafeUUID lookup unexpectedly succeeded') + finally: + uuid.SafeUUID = SafeUUID + assert obj.is_safe is SafeUUID.unknown + ''')) + def test_compatibility(self): import uuid diff --git a/Modules/_uuidmodule.c b/Modules/_uuidmodule.c index b04c39b59aac203..d043d5efcf6cab0 100644 --- a/Modules/_uuidmodule.c +++ b/Modules/_uuidmodule.c @@ -1106,6 +1106,9 @@ Uuid_get_is_safe(PyObject *op, void *Py_UNUSED(closure)) uuid_state *state = get_uuid_state_by_cls(Py_TYPE(self)); if (self->is_safe == NULL) { PyObject *SafeUUID = get_SafeUUID(state); + if (SafeUUID == NULL) { + return NULL; + } PyObject *ret = PyObject_GetAttrString(SafeUUID, "unknown"); Py_DECREF(SafeUUID); return ret; From 0802f7b3672f55ace506331630ef721d43db2708 Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Wed, 21 Oct 2026 11:50:50 -0700 Subject: [PATCH 32/38] gh-139122: Honor subclass string conversion in UUID repr --- Lib/test/test_uuid.py | 8 ++++++++ Modules/_uuidmodule.c | 9 +-------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Lib/test/test_uuid.py b/Lib/test/test_uuid.py index e1f017c1138a9bf..4eb1b71dd63caaf 100755 --- a/Lib/test/test_uuid.py +++ b/Lib/test/test_uuid.py @@ -303,6 +303,14 @@ def test_UUID_strip_braces(self): with self.assertRaises(ValueError): self.uuid.UUID(text) + def test_repr_subclass_str(self): + # repr() must call the subclass's __str__, as the Python version does. + class CustomUUID(self.uuid.UUID): + def __str__(self): + return 'custom uuid' + + self.assertEqual(repr(CustomUUID(int=1)), "CustomUUID('custom uuid')") + def test_exceptions(self): badvalue = lambda f: self.assertRaises(ValueError, f) badtype = lambda f: self.assertRaises(TypeError, f) diff --git a/Modules/_uuidmodule.c b/Modules/_uuidmodule.c index d043d5efcf6cab0..26e9bee067613f3 100644 --- a/Modules/_uuidmodule.c +++ b/Modules/_uuidmodule.c @@ -1487,20 +1487,13 @@ Uuid_str(PyObject *self) static PyObject * Uuid_repr(PyObject *self) { - PyObject *str_obj = Uuid_str(self); - if (str_obj == NULL) { - return NULL; - } - // Get the class name (sadly can't use tp_name -- we don't need the full name) PyObject *cls_name = PyObject_GetAttrString((PyObject *)Py_TYPE(self), "__name__"); if (cls_name == NULL) { - Py_DECREF(str_obj); return NULL; } - PyObject *repr = PyUnicode_FromFormat("%U('%U')", cls_name, str_obj); - Py_DECREF(str_obj); + PyObject *repr = PyUnicode_FromFormat("%U('%S')", cls_name, self); Py_DECREF(cls_name); return repr; } From 54e040aa6e94d7040d532717922fdb7910897f0a Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Wed, 21 Oct 2026 11:54:26 -0700 Subject: [PATCH 33/38] Apply batched suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply review suggestions Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> Co-authored-by: Yury Selivanov --- Doc/library/uuid.rst | 2 +- Lib/uuid.py | 2 +- .../2025-09-18-14-13-00.gh-issue-139122.m3lp66.rst | 4 ++-- Modules/_uuidmodule.c | 11 +++++------ 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/Doc/library/uuid.rst b/Doc/library/uuid.rst index 6e21b6f99f877d4..819be3233d0a02a 100644 --- a/Doc/library/uuid.rst +++ b/Doc/library/uuid.rst @@ -69,7 +69,7 @@ which relays any information about the UUID's safety, using this enumeration: variant and version number set according to :rfc:`9562`, overriding bits in the given *hex*, *bytes*, *bytes_le*, *fields*, or *int*. - .. versionchanged:: 3.16 + .. versionchanged:: next Passing an object other than :class:`bytes` as *bytes* or *bytes_le* now consistently raises :exc:`TypeError`. Previously, some invalid inputs raised :exc:`AssertionError` or :exc:`ValueError`. diff --git a/Lib/uuid.py b/Lib/uuid.py index f8e602b725c97fd..2070f63fa54adf8 100644 --- a/Lib/uuid.py +++ b/Lib/uuid.py @@ -864,7 +864,7 @@ def uuid6(node=None, clock_seq=None): _last_counter_v7 = 0 # 42-bit counter def _uuid7_get_counter_and_tail(): - rand = int.from_bytes(_gen_random(10), 'big') + rand = int.from_bytes(_gen_random(10)) # 42-bit counter with MSB set to 0 counter = (rand >> 32) & 0x1ff_ffff_ffff diff --git a/Misc/NEWS.d/next/Library/2025-09-18-14-13-00.gh-issue-139122.m3lp66.rst b/Misc/NEWS.d/next/Library/2025-09-18-14-13-00.gh-issue-139122.m3lp66.rst index bc3fd369dd5566c..54e116cb6a02e9b 100644 --- a/Misc/NEWS.d/next/Library/2025-09-18-14-13-00.gh-issue-139122.m3lp66.rst +++ b/Misc/NEWS.d/next/Library/2025-09-18-14-13-00.gh-issue-139122.m3lp66.rst @@ -1,2 +1,2 @@ -Reimplement base UUID type, uuid4(), and uuid7() in C - +:mod:`uuid`: Reimplement :class:`~uuid.UUID` objects, :func:`~uuid.uuid4`, +and :func:`~uuid.uuid7` in C. diff --git a/Modules/_uuidmodule.c b/Modules/_uuidmodule.c index 26e9bee067613f3..d0273925e74f287 100644 --- a/Modules/_uuidmodule.c +++ b/Modules/_uuidmodule.c @@ -1830,12 +1830,11 @@ uuid_exec(PyObject *module) #undef ADD_INT - state->uuid_type = (PyTypeObject *)PyType_FromMetaclass( - NULL, - module, - &Uuid_spec, - NULL - ); + state->uuid_type = (PyTypeObject *)PyType_FromModuleAndSpec( + module, + &Uuid_spec, + NULL + ); if (state->uuid_type == NULL) { return -1; } From f42e84a840a339ba145217a8da807b3018a29979 Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Wed, 21 Oct 2026 11:55:49 -0700 Subject: [PATCH 34/38] gh-139122: Normalize line endings in UUID review suggestions --- Doc/library/uuid.rst | 2 +- .../2025-09-18-14-13-00.gh-issue-139122.m3lp66.rst | 4 ++-- Modules/_uuidmodule.c | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Doc/library/uuid.rst b/Doc/library/uuid.rst index 819be3233d0a02a..2d266c2d433c604 100644 --- a/Doc/library/uuid.rst +++ b/Doc/library/uuid.rst @@ -69,7 +69,7 @@ which relays any information about the UUID's safety, using this enumeration: variant and version number set according to :rfc:`9562`, overriding bits in the given *hex*, *bytes*, *bytes_le*, *fields*, or *int*. - .. versionchanged:: next + .. versionchanged:: next Passing an object other than :class:`bytes` as *bytes* or *bytes_le* now consistently raises :exc:`TypeError`. Previously, some invalid inputs raised :exc:`AssertionError` or :exc:`ValueError`. diff --git a/Misc/NEWS.d/next/Library/2025-09-18-14-13-00.gh-issue-139122.m3lp66.rst b/Misc/NEWS.d/next/Library/2025-09-18-14-13-00.gh-issue-139122.m3lp66.rst index 54e116cb6a02e9b..cb656aaf793c630 100644 --- a/Misc/NEWS.d/next/Library/2025-09-18-14-13-00.gh-issue-139122.m3lp66.rst +++ b/Misc/NEWS.d/next/Library/2025-09-18-14-13-00.gh-issue-139122.m3lp66.rst @@ -1,2 +1,2 @@ -:mod:`uuid`: Reimplement :class:`~uuid.UUID` objects, :func:`~uuid.uuid4`, -and :func:`~uuid.uuid7` in C. +:mod:`uuid`: Reimplement :class:`~uuid.UUID` objects, :func:`~uuid.uuid4`, +and :func:`~uuid.uuid7` in C. diff --git a/Modules/_uuidmodule.c b/Modules/_uuidmodule.c index d0273925e74f287..3c6935628e70dc2 100644 --- a/Modules/_uuidmodule.c +++ b/Modules/_uuidmodule.c @@ -1830,11 +1830,11 @@ uuid_exec(PyObject *module) #undef ADD_INT - state->uuid_type = (PyTypeObject *)PyType_FromModuleAndSpec( - module, - &Uuid_spec, - NULL - ); + state->uuid_type = (PyTypeObject *)PyType_FromModuleAndSpec( + module, + &Uuid_spec, + NULL + ); if (state->uuid_type == NULL) { return -1; } From 95b85444042226fa02f5a16654c87949ffe0de82 Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Wed, 21 Oct 2026 11:59:01 -0700 Subject: [PATCH 35/38] gh-139122: Test UUID state restoration with frozendict --- Lib/test/test_uuid.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_uuid.py b/Lib/test/test_uuid.py index 4eb1b71dd63caaf..2a2354133f1e18a 100755 --- a/Lib/test/test_uuid.py +++ b/Lib/test/test_uuid.py @@ -503,7 +503,7 @@ def test_setstate_mapping(self): for safety in self.uuid.SafeUUID ) with support.swap_item(sys.modules, 'uuid', self.uuid): - for mapping in (dict, UserDict, MappingProxyType): + for mapping in (dict, frozendict, UserDict, MappingProxyType): for safety_state, expected_safety in safety_states: with self.subTest(mapping=mapping, state=safety_state): u = self.uuid.UUID(int=0, is_safe=self.uuid.SafeUUID.safe) @@ -514,7 +514,7 @@ def test_setstate_mapping(self): def test_setstate_missing_int(self): # A missing required key must retain the mapping's KeyError and key. - for mapping in (dict, UserDict, MappingProxyType): + for mapping in (dict, frozendict, UserDict, MappingProxyType): with self.subTest(mapping=mapping): u = self.uuid.UUID(int=1) with self.assertRaises(KeyError) as cm: From ce35192519583868b5238025172ab066efb9f4b3 Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Wed, 21 Oct 2026 12:02:33 -0700 Subject: [PATCH 36/38] gh-139122: Use shared hex conversion helper for UUID.hex --- Modules/_uuidmodule.c | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Modules/_uuidmodule.c b/Modules/_uuidmodule.c index 3c6935628e70dc2..b0d064d8cff0068 100644 --- a/Modules/_uuidmodule.c +++ b/Modules/_uuidmodule.c @@ -19,6 +19,7 @@ #include "pycore_long.h" // _PyLong_AsByteArray(), _PyLong_GetOne() #include "pycore_pylifecycle.h" // _PyOS_URandom() #include "pycore_runtime_init.h" // _Py_ID() +#include "pycore_strhex.h" // _Py_strhex() #if defined(HAVE_UUID_H) // AIX, FreeBSD, libuuid with pkgconf @@ -1120,11 +1121,7 @@ static PyObject * Uuid_get_hex(PyObject *op, void *Py_UNUSED(closure)) { uuidobject *self = uuidobject_CAST(op); - char hex[UUID_SIZE * 2]; - for (int i = 0; i < UUID_SIZE; i++) { - byte_to_hex(self->bytes[i], &hex[i * 2]); - } - return PyUnicode_FromStringAndSize(hex, sizeof(hex)); + return _Py_strhex((const char *)self->bytes, sizeof(self->bytes)); } static PyObject * From 8ebad36d7fe2f29919919472c17873c8a6990c71 Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Wed, 21 Oct 2026 12:05:38 -0700 Subject: [PATCH 37/38] gh-139122: Format UUIDs directly into the result string --- Modules/_uuidmodule.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Modules/_uuidmodule.c b/Modules/_uuidmodule.c index b0d064d8cff0068..e957071a4ecef95 100644 --- a/Modules/_uuidmodule.c +++ b/Modules/_uuidmodule.c @@ -1452,7 +1452,11 @@ Uuid_str(PyObject *self) uuidobject *uuid = uuidobject_CAST(self); // UUID string format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (36 chars) - char str[36]; + PyObject *result = PyUnicode_New(36, 127); + if (result == NULL) { + return NULL; + } + char *str = (char *)PyUnicode_1BYTE_DATA(result); for (int i = 0; i < 4; i++) { byte_to_hex(uuid->bytes[i], &str[i * 2]); @@ -1478,7 +1482,7 @@ Uuid_str(PyObject *self) byte_to_hex(uuid->bytes[i], &str[24 + (i - 10) * 2]); } - return PyUnicode_FromStringAndSize(str, sizeof(str)); + return result; } static PyObject * From 9e407676523235089fd8452e3d82bb749d2c32bd Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Wed, 21 Oct 2026 12:08:22 -0700 Subject: [PATCH 38/38] gh-139122: Narrow UUID exception assertions and clean up test spacing --- Lib/test/test_uuid.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_uuid.py b/Lib/test/test_uuid.py index 2a2354133f1e18a..ee6e57b93966a23 100755 --- a/Lib/test/test_uuid.py +++ b/Lib/test/test_uuid.py @@ -533,8 +533,9 @@ def __getitem__(self, key): for bad_key in ('int', 'is_safe'): with self.subTest(key=bad_key): u = self.uuid.UUID(int=1) + state = State(int=2, is_safe=0) with self.assertRaisesRegex(RuntimeError, 'lookup failed'): - u.__setstate__(State(int=2, is_safe=0)) + u.__setstate__(state) def test_setstate_contains_error(self): class State(dict): @@ -543,8 +544,9 @@ def __contains__(self, key): # Errors checking for the optional key must propagate as well. u = self.uuid.UUID(int=1) + state = State(int=2) with self.assertRaisesRegex(RuntimeError, 'membership test failed'): - u.__setstate__(State(int=2)) + u.__setstate__(state) def test_setstate_invalid_is_safe(self): u = self.uuid.UUID(int=0) @@ -1748,9 +1750,9 @@ def __init__(self, *, start_at=0, inc_by=0, seed=0): self._inc_by = inc_by self._rnd = random.Random(seed) - def random_func (self, size) : + def random_func(self, size): ret = b'' - for _ in range(size) : + for _ in range(size): ret += self._rnd.getrandbits(8).to_bytes(1, 'big') return ret