]> granicus.if.org Git - python/commitdiff
bpo-22807: Expose platform UUID generation safety information. (#138)
authorBarry Warsaw <barry@python.org>
Sat, 18 Feb 2017 20:45:49 +0000 (15:45 -0500)
committerGitHub <noreply@github.com>
Sat, 18 Feb 2017 20:45:49 +0000 (15:45 -0500)
bpo-22807: Expose platform UUID generation safety information.

Doc/library/uuid.rst
Lib/test/test_uuid.py
Lib/uuid.py
Misc/NEWS

index edbf832529a64f9b80a6b1578bb129c5df2e9c1b..ea9ea7dc7d9b823a877aca961fae554c0fdcd8b9 100644 (file)
@@ -19,8 +19,30 @@ If all you want is a unique ID, you should probably call :func:`uuid1` or
 a UUID containing the computer's network address.  :func:`uuid4` creates a
 random UUID.
 
+Depending on support from the underlying platform, :func:`uuid1` may or may
+not return a "safe" UUID.  A safe UUID is one which is generated using
+synchronization methods that ensure no two processes can obtain the same
+UUID.  All instances of :class:`UUID` have an :attr:`is_safe` attribute
+which relays any information about the UUID's safety, using this enumeration:
 
-.. class:: UUID(hex=None, bytes=None, bytes_le=None, fields=None, int=None, version=None)
+.. class:: SafeUUID
+
+   .. versionadded:: 3.7
+
+   .. attribute:: SafeUUID.safe
+
+      The UUID was generated by the platform in a multiprocessing-safe way.
+
+   .. attribute:: SafeUUID.unsafe
+
+      The UUID was not generated in a multiprocessing-safe way.
+
+   .. attribute:: SafeUUID.unknown
+
+      The platform does not provide information on whether the UUID was
+      generated safely or not.
+
+.. class:: UUID(hex=None, bytes=None, bytes_le=None, fields=None, int=None, version=None, *, is_safe=SafeUUID.unknown)
 
    Create a UUID from either a string of 32 hexadecimal digits, a string of 16
    bytes as the *bytes* argument, a string of 16 bytes in little-endian order as
@@ -120,6 +142,13 @@ random UUID.
    The UUID version number (1 through 5, meaningful only when the variant is
    :const:`RFC_4122`).
 
+.. attribute:: UUID.is_safe
+
+   An enumeration of :class:`SafeUUID` which indicates whether the platform
+   generated the UUID in a multiprocessing-safe way.
+
+   .. versionadded:: 3.7
+
 The :mod:`uuid` module defines the following functions:
 
 
index 47248f92c7b6af3275029f702526d21316d091ec..c912c02c2e17d06c008313e8dcc771fd9b9d577a 100644 (file)
@@ -340,6 +340,46 @@ class TestUUID(unittest.TestCase):
         equal(((u.clock_seq_hi_variant & 0x3f) << 8) |
                          u.clock_seq_low, 0x3fff)
 
+    @unittest.skipUnless(uuid._uuid_generate_time.restype is not None,
+                         'requires uuid_generate_time_safe(3)')
+    @unittest.skipUnless(importable('ctypes'), 'requires ctypes')
+    def test_uuid1_safe(self):
+        u = uuid.uuid1()
+        # uuid_generate_time_safe() may return 0 or -1 but what it returns is
+        # dependent on the underlying platform support.  At least it cannot be
+        # unknown (unless I suppose the platform is buggy).
+        self.assertNotEqual(u.is_safe, uuid.SafeUUID.unknown)
+
+    @unittest.skipUnless(importable('ctypes'), 'requires ctypes')
+    def test_uuid1_unknown(self):
+        # Even if the platform has uuid_generate_time_safe(), let's mock it to
+        # be uuid_generate_time() and ensure the safety is unknown.
+        with unittest.mock.patch.object(uuid._uuid_generate_time,
+                                        'restype', None):
+            u = uuid.uuid1()
+            self.assertEqual(u.is_safe, uuid.SafeUUID.unknown)
+
+    @unittest.skipUnless(importable('ctypes'), 'requires ctypes')
+    def test_uuid1_is_safe(self):
+        with unittest.mock.patch.object(uuid._uuid_generate_time,
+                                        'restype', lambda x: 0):
+            u = uuid.uuid1()
+            self.assertEqual(u.is_safe, uuid.SafeUUID.safe)
+
+    @unittest.skipUnless(importable('ctypes'), 'requires ctypes')
+    def test_uuid1_is_unsafe(self):
+        with unittest.mock.patch.object(uuid._uuid_generate_time,
+                                        'restype', lambda x: -1):
+            u = uuid.uuid1()
+            self.assertEqual(u.is_safe, uuid.SafeUUID.unsafe)
+
+    @unittest.skipUnless(importable('ctypes'), 'requires ctypes')
+    def test_uuid1_bogus_return_value(self):
+        with unittest.mock.patch.object(uuid._uuid_generate_time,
+                                        'restype', lambda x: 3):
+            u = uuid.uuid1()
+            self.assertEqual(u.is_safe, uuid.SafeUUID.unknown)
+
     def test_uuid3(self):
         equal = self.assertEqual
 
index 200c800b34e4bbd36e124c7b5326126fba5716e9..d4259ae0b3bd676338fd979b18c243d02ac4316d 100644 (file)
@@ -46,6 +46,9 @@ Typical usage:
 
 import os
 
+from enum import Enum
+
+
 __author__ = 'Ka-Ping Yee <ping@zesty.ca>'
 
 RESERVED_NCS, RFC_4122, RESERVED_MICROSOFT, RESERVED_FUTURE = [
@@ -55,7 +58,14 @@ RESERVED_NCS, RFC_4122, RESERVED_MICROSOFT, RESERVED_FUTURE = [
 int_ = int      # The built-in int type
 bytes_ = bytes  # The built-in bytes type
 
-class UUID(object):
+
+class SafeUUID(Enum):
+    safe = 0
+    unsafe = -1
+    unknown = 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.
     Converting a UUID to a string with str() yields something in the form
@@ -101,10 +111,15 @@ class UUID(object):
 
         version     the UUID version number (1 through 5, meaningful only
                     when the variant is RFC_4122)
+
+        is_safe     An enum indicating whether the UUID has been generated in
+                    a way that is safe for multiprocessing applications, via
+                    uuid_generate_time_safe(3).
     """
 
     def __init__(self, hex=None, bytes=None, bytes_le=None, fields=None,
-                       int=None, version=None):
+                       int=None, version=None,
+                       *, is_safe=SafeUUID.unknown):
         r"""Create a UUID from either a string of 32 hexadecimal digits,
         a string of 16 bytes as the 'bytes' argument, a string of 16 bytes
         in little-endian order as the 'bytes_le' argument, a tuple of six
@@ -128,6 +143,10 @@ class UUID(object):
         be given.  The 'version' argument is optional; if given, the resulting
         UUID will have its variant and version set according to RFC 4122,
         overriding the given 'hex', 'bytes', 'bytes_le', 'fields', or 'int'.
+
+        is_safe is an enum exposed as an attribute on the instance.  It
+        indicates whether the UUID has been generated in a way that is safe
+        for multiprocessing applications, via uuid_generate_time_safe(3).
         """
 
         if [hex, bytes, bytes_le, fields, int].count(None) != 4:
@@ -182,6 +201,7 @@ class UUID(object):
             int &= ~(0xf000 << 64)
             int |= version << 76
         self.__dict__['int'] = int
+        self.__dict__['is_safe'] = is_safe
 
     def __eq__(self, other):
         if isinstance(other, UUID):
@@ -472,10 +492,17 @@ try:
     for libname in _libnames:
         try:
             lib = ctypes.CDLL(ctypes.util.find_library(libname))
-        except Exception:
+        except Exception:                           # pragma: nocover
             continue
-        if hasattr(lib, 'uuid_generate_time'):
+        # Try to find the safe variety first.
+        if hasattr(lib, 'uuid_generate_time_safe'):
+            _uuid_generate_time = lib.uuid_generate_time_safe
+            # int uuid_generate_time_safe(uuid_t out);
+            break
+        elif hasattr(lib, 'uuid_generate_time'):    # pragma: nocover
             _uuid_generate_time = lib.uuid_generate_time
+            # void uuid_generate_time(uuid_t out);
+            _uuid_generate_time.restype = None
             break
     del _libnames
 
@@ -566,8 +593,12 @@ def uuid1(node=None, clock_seq=None):
     # use UuidCreate here because its UUIDs don't conform to RFC 4122).
     if _uuid_generate_time and node is clock_seq is None:
         _buffer = ctypes.create_string_buffer(16)
-        _uuid_generate_time(_buffer)
-        return UUID(bytes=bytes_(_buffer.raw))
+        safely_generated = _uuid_generate_time(_buffer)
+        try:
+            is_safe = SafeUUID(safely_generated)
+        except ValueError:
+            is_safe = SafeUUID.unknown
+        return UUID(bytes=bytes_(_buffer.raw), is_safe=is_safe)
 
     global _last_timestamp
     import time
index 53f1dc6d7f0c4f3f2a644432d8fe685e96175558..9d61b4623e659c579e559a563b92701aa0590d48 100644 (file)
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -229,6 +229,10 @@ Extension Modules
 Library
 -------
 
+- bpo-22807: Add uuid.SafeUUID and uuid.UUID.is_safe to relay information from
+  the platform about whether generated UUIDs are generated with a
+  multiprocessing safe method.
+
 - bpo-29576: Improve some deprecations in importlib. Some deprecated methods
   now emit DeprecationWarnings and have better descriptive messages.