From 8c130d7f8114158f5b94749032ec0c17dba96f83 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Sat, 18 Feb 2017 15:45:49 -0500 Subject: bpo-22807: Expose platform UUID generation safety information. (#138) bpo-22807: Expose platform UUID generation safety information. --- Doc/library/uuid.rst | 31 ++++++++++++++++++++++++++++++- Lib/test/test_uuid.py | 40 ++++++++++++++++++++++++++++++++++++++++ Lib/uuid.py | 43 +++++++++++++++++++++++++++++++++++++------ Misc/NEWS | 4 ++++ 4 files changed, 111 insertions(+), 7 deletions(-) diff --git a/Doc/library/uuid.rst b/Doc/library/uuid.rst index edbf832..ea9ea7d 100644 --- a/Doc/library/uuid.rst +++ b/Doc/library/uuid.rst @@ -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: diff --git a/Lib/test/test_uuid.py b/Lib/test/test_uuid.py index 47248f9..c912c02 100644 --- a/Lib/test/test_uuid.py +++ b/Lib/test/test_uuid.py @@ -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 diff --git a/Lib/uuid.py b/Lib/uuid.py index 200c800..d4259ae 100644 --- a/Lib/uuid.py +++ b/Lib/uuid.py @@ -46,6 +46,9 @@ Typical usage: import os +from enum import Enum + + __author__ = 'Ka-Ping Yee ' 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 diff --git a/Misc/NEWS b/Misc/NEWS index 53f1dc6..9d61b46 100644 --- 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. -- cgit v0.12