Skip to content

Commit 8c130d7

Browse files
authored
bpo-22807: Expose platform UUID generation safety information. (#138)
bpo-22807: Expose platform UUID generation safety information.
1 parent ace5c0f commit 8c130d7

File tree

4 files changed

+111
-7
lines changed

4 files changed

+111
-7
lines changed

‎Doc/library/uuid.rst‎

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,30 @@ If all you want is a unique ID, you should probably call :func:`uuid1` or
1919
a UUID containing the computer's network address. :func:`uuid4` creates a
2020
random UUID.
2121

22+
Depending on support from the underlying platform, :func:`uuid1` may or may
23+
not return a "safe" UUID. A safe UUID is one which is generated using
24+
synchronization methods that ensure no two processes can obtain the same
25+
UUID. All instances of :class:`UUID` have an :attr:`is_safe` attribute
26+
which relays any information about the UUID's safety, using this enumeration:
2227

23-
.. class:: UUID(hex=None, bytes=None, bytes_le=None, fields=None, int=None, version=None)
28+
.. class:: SafeUUID
29+
30+
.. versionadded:: 3.7
31+
32+
.. attribute:: SafeUUID.safe
33+
34+
The UUID was generated by the platform in a multiprocessing-safe way.
35+
36+
.. attribute:: SafeUUID.unsafe
37+
38+
The UUID was not generated in a multiprocessing-safe way.
39+
40+
.. attribute:: SafeUUID.unknown
41+
42+
The platform does not provide information on whether the UUID was
43+
generated safely or not.
44+
45+
.. class:: UUID(hex=None, bytes=None, bytes_le=None, fields=None, int=None, version=None, *, is_safe=SafeUUID.unknown)
2446

2547
Create a UUID from either a string of 32 hexadecimal digits, a string of 16
2648
bytes as the *bytes* argument, a string of 16 bytes in little-endian order as
@@ -120,6 +142,13 @@ random UUID.
120142
The UUID version number (1 through 5, meaningful only when the variant is
121143
:const:`RFC_4122`).
122144

145+
.. attribute:: UUID.is_safe
146+
147+
An enumeration of :class:`SafeUUID` which indicates whether the platform
148+
generated the UUID in a multiprocessing-safe way.
149+
150+
.. versionadded:: 3.7
151+
123152
The :mod:`uuid` module defines the following functions:
124153

125154

‎Lib/test/test_uuid.py‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,46 @@ def test_uuid1(self):
340340
equal(((u.clock_seq_hi_variant & 0x3f) << 8) |
341341
u.clock_seq_low, 0x3fff)
342342

343+
@unittest.skipUnless(uuid._uuid_generate_time.restype is not None,
344+
'requires uuid_generate_time_safe(3)')
345+
@unittest.skipUnless(importable('ctypes'), 'requires ctypes')
346+
def test_uuid1_safe(self):
347+
u = uuid.uuid1()
348+
# uuid_generate_time_safe() may return 0 or -1 but what it returns is
349+
# dependent on the underlying platform support. At least it cannot be
350+
# unknown (unless I suppose the platform is buggy).
351+
self.assertNotEqual(u.is_safe, uuid.SafeUUID.unknown)
352+
353+
@unittest.skipUnless(importable('ctypes'), 'requires ctypes')
354+
def test_uuid1_unknown(self):
355+
# Even if the platform has uuid_generate_time_safe(), let's mock it to
356+
# be uuid_generate_time() and ensure the safety is unknown.
357+
with unittest.mock.patch.object(uuid._uuid_generate_time,
358+
'restype', None):
359+
u = uuid.uuid1()
360+
self.assertEqual(u.is_safe, uuid.SafeUUID.unknown)
361+
362+
@unittest.skipUnless(importable('ctypes'), 'requires ctypes')
363+
def test_uuid1_is_safe(self):
364+
with unittest.mock.patch.object(uuid._uuid_generate_time,
365+
'restype', lambda x: 0):
366+
u = uuid.uuid1()
367+
self.assertEqual(u.is_safe, uuid.SafeUUID.safe)
368+
369+
@unittest.skipUnless(importable('ctypes'), 'requires ctypes')
370+
def test_uuid1_is_unsafe(self):
371+
with unittest.mock.patch.object(uuid._uuid_generate_time,
372+
'restype', lambda x: -1):
373+
u = uuid.uuid1()
374+
self.assertEqual(u.is_safe, uuid.SafeUUID.unsafe)
375+
376+
@unittest.skipUnless(importable('ctypes'), 'requires ctypes')
377+
def test_uuid1_bogus_return_value(self):
378+
with unittest.mock.patch.object(uuid._uuid_generate_time,
379+
'restype', lambda x: 3):
380+
u = uuid.uuid1()
381+
self.assertEqual(u.is_safe, uuid.SafeUUID.unknown)
382+
343383
def test_uuid3(self):
344384
equal = self.assertEqual
345385

‎Lib/uuid.py‎

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@
4646

4747
import os
4848

49+
from enum import Enum
50+
51+
4952
__author__ = 'Ka-Ping Yee <[email protected]>'
5053

5154
RESERVED_NCS, RFC_4122, RESERVED_MICROSOFT, RESERVED_FUTURE = [
@@ -55,7 +58,14 @@
5558
int_ = int # The built-in int type
5659
bytes_ = bytes # The built-in bytes type
5760

58-
class UUID(object):
61+
62+
class SafeUUID(Enum):
63+
safe = 0
64+
unsafe = -1
65+
unknown = None
66+
67+
68+
class UUID:
5969
"""Instances of the UUID class represent UUIDs as specified in RFC 4122.
6070
UUID objects are immutable, hashable, and usable as dictionary keys.
6171
Converting a UUID to a string with str() yields something in the form
@@ -101,10 +111,15 @@ class UUID(object):
101111
102112
version the UUID version number (1 through 5, meaningful only
103113
when the variant is RFC_4122)
114+
115+
is_safe An enum indicating whether the UUID has been generated in
116+
a way that is safe for multiprocessing applications, via
117+
uuid_generate_time_safe(3).
104118
"""
105119

106120
def __init__(self, hex=None, bytes=None, bytes_le=None, fields=None,
107-
int=None, version=None):
121+
int=None, version=None,
122+
*, is_safe=SafeUUID.unknown):
108123
r"""Create a UUID from either a string of 32 hexadecimal digits,
109124
a string of 16 bytes as the 'bytes' argument, a string of 16 bytes
110125
in little-endian order as the 'bytes_le' argument, a tuple of six
@@ -128,6 +143,10 @@ def __init__(self, hex=None, bytes=None, bytes_le=None, fields=None,
128143
be given. The 'version' argument is optional; if given, the resulting
129144
UUID will have its variant and version set according to RFC 4122,
130145
overriding the given 'hex', 'bytes', 'bytes_le', 'fields', or 'int'.
146+
147+
is_safe is an enum exposed as an attribute on the instance. It
148+
indicates whether the UUID has been generated in a way that is safe
149+
for multiprocessing applications, via uuid_generate_time_safe(3).
131150
"""
132151

133152
if [hex, bytes, bytes_le, fields, int].count(None) != 4:
@@ -182,6 +201,7 @@ def __init__(self, hex=None, bytes=None, bytes_le=None, fields=None,
182201
int &= ~(0xf000 << 64)
183202
int |= version << 76
184203
self.__dict__['int'] = int
204+
self.__dict__['is_safe'] = is_safe
185205

186206
def __eq__(self, other):
187207
if isinstance(other, UUID):
@@ -472,10 +492,17 @@ def _netbios_getnode():
472492
for libname in _libnames:
473493
try:
474494
lib = ctypes.CDLL(ctypes.util.find_library(libname))
475-
except Exception:
495+
except Exception: # pragma: nocover
476496
continue
477-
if hasattr(lib, 'uuid_generate_time'):
497+
# Try to find the safe variety first.
498+
if hasattr(lib, 'uuid_generate_time_safe'):
499+
_uuid_generate_time = lib.uuid_generate_time_safe
500+
# int uuid_generate_time_safe(uuid_t out);
501+
break
502+
elif hasattr(lib, 'uuid_generate_time'): # pragma: nocover
478503
_uuid_generate_time = lib.uuid_generate_time
504+
# void uuid_generate_time(uuid_t out);
505+
_uuid_generate_time.restype = None
479506
break
480507
del _libnames
481508

@@ -566,8 +593,12 @@ def uuid1(node=None, clock_seq=None):
566593
# use UuidCreate here because its UUIDs don't conform to RFC 4122).
567594
if _uuid_generate_time and node is clock_seq is None:
568595
_buffer = ctypes.create_string_buffer(16)
569-
_uuid_generate_time(_buffer)
570-
return UUID(bytes=bytes_(_buffer.raw))
596+
safely_generated = _uuid_generate_time(_buffer)
597+
try:
598+
is_safe = SafeUUID(safely_generated)
599+
except ValueError:
600+
is_safe = SafeUUID.unknown
601+
return UUID(bytes=bytes_(_buffer.raw), is_safe=is_safe)
571602

572603
global _last_timestamp
573604
import time

‎Misc/NEWS‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,10 @@ Extension Modules
229229
Library
230230
-------
231231

232+
- bpo-22807: Add uuid.SafeUUID and uuid.UUID.is_safe to relay information from
233+
the platform about whether generated UUIDs are generated with a
234+
multiprocessing safe method.
235+
232236
- bpo-29576: Improve some deprecations in importlib. Some deprecated methods
233237
now emit DeprecationWarnings and have better descriptive messages.
234238

0 commit comments

Comments
 (0)