Repository: mikedh/trimesh
Pull request #1536 (2022-03-28 22:05:45)

Change unique_id() implementation. And add a test.
Addresses #1517 


Patch:
diff --git a/trimesh/constants.py b/trimesh/constants.py
index 0ed0d401..c1f0a507 100644
--- a/trimesh/constants.py
+++ b/trimesh/constants.py
@@ -1,6 +1,13 @@
 import numpy as np
 
-from .util import log, now
+from .util import log, PY3
+
+if PY3:
+    # will be the highest granularity clock available
+    from time import perf_counter as now
+else:
+    # perf_counter not available on python 2
+    from time import time as now
 
 
 class ToleranceMesh(object):
diff --git a/trimesh/util.py b/trimesh/util.py
index d863f234..30ab47ce 100644
--- a/trimesh/util.py
+++ b/trimesh/util.py
@@ -13,7 +13,9 @@ import abc
 import sys
 import copy
 import json
+import uuid
 import base64
+import random
 import shutil
 import logging
 import hashlib
@@ -37,8 +39,6 @@ if PY3:
     basestring = str
     # Python 3
     from io import BytesIO, StringIO
-    # will be the highest granularity clock available
-    from time import perf_counter as now
 else:
     # Python 2
     from StringIO import StringIO
@@ -46,8 +46,6 @@ else:
     StringIO.__enter__ = lambda a: a
     StringIO.__exit__ = lambda a, b, c, d: a.close()
     BytesIO = StringIO
-    # perf_counter not available on python 2
-    from time import time as now
 
 
 try:
@@ -2121,44 +2119,30 @@ def write_encoded(file_obj,
     return stuff
 
 
-def unique_id(length=12, increment=0):
+def unique_id(length=12, increment=0, denylist=None):
     """
-    Generate a decent looking alphanumeric unique identifier.
-    First 16 bits are time-incrementing, followed by randomness.
-
-    This function is used as a nicer looking alternative to:
-    >>> uuid.uuid4().hex
-
-    Follows the advice in:
-    https://eager.io/blog/how-long-does-an-id-need-to-be/
+    Generate an alphanumeric unique identifier.
 
     Parameters
     ------------
     length : int
       Length of desired identifier
     increment : int
-      Number to add to header uint16
-      useful if calling this function repeatedly
-      in a tight loop executing faster than time
-      can increment the header
+      Unused. For backwards compatibility
+    denylist : list of str
+      Identifiers that are not allowed
 
     Returns
     ------------
     unique : str
       Unique alphanumeric identifier
     """
-    # head the identifier with 16 bits of time information
-    # this provides locality and reduces collision chances
-    head = np.array((increment + now() * 10) % 2**16,
-                    dtype=np.uint16).tobytes()
-    # get a bunch of random bytes
-    random = np.random.random(int(np.ceil(length / 5))).tobytes()
-    # encode the time header and random information as base64
-    # replace + and / with spaces
-    unique = base64.b64encode(head + random,
-                              b'  ').decode('utf-8')
-    # remove spaces and cut to length
-    unique = unique.replace(' ', '')[:length]
+    unique = str(uuid.UUID(int=random.getrandbits(128), version=4))[:length]
+
+    if denylist is not None:
+        while unique in denylist:
+            unique = str(uuid.UUID(int=random.getrandbits(128), version=4))[:length]
+
     return unique
 
 


Test patch:
diff --git a/tests/test_util.py b/tests/test_util.py
index de382a5a..2c24de9f 100644
--- a/tests/test_util.py
+++ b/tests/test_util.py
@@ -167,6 +167,27 @@ class UtilTests(unittest.TestCase):
                             a.volume * count)
         assert a.md5() == hA
 
+    def test_unique_id(self):
+        num_ids = 10000
+
+        g.trimesh.util.random.seed(0)
+        unique_ids_0 = []
+        for i in range(num_ids):
+            s = g.trimesh.util.unique_id(denylist=unique_ids_0)
+            unique_ids_0.append(s)
+
+        # make sure every id is truly unique
+        assert len(unique_ids_0) == len(g.np.unique(unique_ids_0))
+
+        g.trimesh.util.random.seed(0)
+        unique_ids_1 = []
+        for i in range(num_ids):
+            s = g.trimesh.util.unique_id(denylist=unique_ids_1)
+            unique_ids_1.append(s)
+
+            # make sure id's can be reproduced
+            assert s == unique_ids_0[i]
+
 
 class ContainsTest(unittest.TestCase):