Barry Warsaw pushed to branch master at mailman / Mailman

Commits:
f739cda9 by Aurélien Bompard at 2015-12-26T13:41:30Z
Return predictable token IDs in testing mode.

This commits builds upon the mailman.utilities.uid.UniqueIDFactory to
generate predictable tokens when the testing mode is activated. This
will make VCR tapes more diffable between runs.

- - - - -
9e3fb01c by Aurélien Bompard at 2015-12-26T18:04:25Z
Give the predictable ID factories similar APIs.

- - - - -
0e11f68b by Barry Warsaw at 2015-12-26T21:41:45Z
Tweak the API.

- - - - -


5 changed files:

- src/mailman/model/member.py
- src/mailman/model/pending.py
- src/mailman/model/uid.py
- src/mailman/model/user.py
- src/mailman/utilities/uid.py


Changes:

=====================================
src/mailman/model/member.py
=====================================
--- a/src/mailman/model/member.py
+++ b/src/mailman/model/member.py
@@ -33,7 +33,7 @@ from mailman.interfaces.member import (
     IMember, MemberRole, MembershipError, UnsubscriptionEvent)
 from mailman.interfaces.user import IUser, UnverifiedAddressError
 from mailman.interfaces.usermanager import IUserManager
-from mailman.utilities.uid import UniqueIDFactory
+from mailman.utilities.uid import UIDFactory
 from sqlalchemy import Column, ForeignKey, Integer, Unicode
 from sqlalchemy.orm import relationship
 from zope.component import getUtility
@@ -41,7 +41,7 @@ from zope.event import notify
 from zope.interface import implementer
 
 
-uid_factory = UniqueIDFactory(context='members')
+uid_factory = UIDFactory(context='members')
 
 
 
@@ -65,7 +65,7 @@ class Member(Model):
     _user = relationship('User')
 
     def __init__(self, role, list_id, subscriber):
-        self._member_id = uid_factory.new_uid()
+        self._member_id = uid_factory.new()
         self.role = role
         self.list_id = list_id
         if IAddress.providedBy(subscriber):


=====================================
src/mailman/model/pending.py
=====================================
--- a/src/mailman/model/pending.py
+++ b/src/mailman/model/pending.py
@@ -24,9 +24,6 @@ __all__ = [
 
 
 import json
-import time
-import random
-import hashlib
 
 from lazr.config import as_timedelta
 from mailman.config import config
@@ -35,12 +32,16 @@ from mailman.database.transaction import dbconnection
 from mailman.interfaces.pending import (
     IPendable, IPended, IPendedKeyValue, IPendings)
 from mailman.utilities.datetime import now
+from mailman.utilities.uid import TokenFactory
 from sqlalchemy import Column, DateTime, ForeignKey, Integer, Unicode, and_
 from sqlalchemy.orm import aliased, relationship
 from zope.interface import implementer
 from zope.interface.verify import verifyObject
 
 
+token_factory = TokenFactory()
+
+
 
 @implementer(IPendedKeyValue)
 class PendedKeyValue(Model):
@@ -88,17 +89,8 @@ class Pendings:
         # Calculate the token and the lifetime.
         if lifetime is None:
             lifetime = as_timedelta(config.mailman.pending_request_life)
-        # Calculate a unique token.  Algorithm vetted by the Timbot.  time()
-        # has high resolution on Linux, clock() on Windows.  random gives us
-        # about 45 bits in Python 2.2, 53 bits on Python 2.3.  The time and
-        # clock values basically help obscure the random number generator, as
-        # does the hash calculation.  The integral parts of the time values
-        # are discarded because they're the most predictable bits.
         for attempts in range(3):
-            right_now = time.time()
-            x = random.random() + right_now % 1.0 + time.clock() % 1.0
-            # Use sha1 because it produces shorter strings.
-            token = hashlib.sha1(repr(x).encode('utf-8')).hexdigest()
+            token = token_factory.new()
             # In practice, we'll never get a duplicate, but we'll be anal
             # about checking anyway.
             if store.query(Pended).filter_by(token=token).count() == 0:


=====================================
src/mailman/model/uid.py
=====================================
--- a/src/mailman/model/uid.py
+++ b/src/mailman/model/uid.py
@@ -36,8 +36,8 @@ class UID(Model):
     This is used so that unique ids don't have to be tracked by each
     individual model object that uses them.  So for example, when a user is
     deleted, we don't have to keep separate track of its uid to prevent it
-    from ever being used again.  This class, hooked up to the
-    `UniqueIDFactory` serves that purpose.
+    from ever being used again.  This class, hooked up to the `UIDFactory`
+    serves that purpose.
 
     There is no interface for this class, because it's purely an internal
     implementation detail.


=====================================
src/mailman/model/user.py
=====================================
--- a/src/mailman/model/user.py
+++ b/src/mailman/model/user.py
@@ -34,14 +34,14 @@ from mailman.model.address import Address
 from mailman.model.preferences import Preferences
 from mailman.model.roster import Memberships
 from mailman.utilities.datetime import factory as date_factory
-from mailman.utilities.uid import UniqueIDFactory
+from mailman.utilities.uid import UIDFactory
 from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, Unicode
 from sqlalchemy.orm import relationship, backref
 from zope.event import notify
 from zope.interface import implementer
 
 
-uid_factory = UniqueIDFactory(context='users')
+uid_factory = UIDFactory(context='users')
 
 
 
@@ -80,7 +80,7 @@ class User(Model):
     def __init__(self, store, display_name=None, preferences=None):
         super(User, self).__init__()
         self._created_on = date_factory.now()
-        user_id = uid_factory.new_uid()
+        user_id = uid_factory.new()
         assert store.query(User).filter_by(_user_id=user_id).count() == 0, (
             'Duplicate user id {0}'.format(user_id))
         self._user_id = user_id


=====================================
src/mailman/utilities/uid.py
=====================================
--- a/src/mailman/utilities/uid.py
+++ b/src/mailman/utilities/uid.py
@@ -22,14 +22,17 @@ and whatnot.  These are better instrumented for testing 
purposes.
 """
 
 __all__ = [
-    'UniqueIDFactory',
-    'factory',
+    'UIDFactory',
+    'TokenFactory',
     ]
 
 
 import os
+import time
 import uuid
 import errno
+import random
+import hashlib
 
 from flufl.lock import Lock
 from mailman.config import config
@@ -38,8 +41,12 @@ from mailman.testing import layers
 
 
 
-class UniqueIDFactory:
-    """A factory for unique ids."""
+class _PredictableIDGenerator:
+    """Base class factory.
+
+    This factory provides a base class for unique ids that need to have
+    predictable values in testing mode.
+    """
 
     def __init__(self, context=None):
         # We can't call reset() when the factory is created below, because
@@ -63,14 +70,10 @@ class UniqueIDFactory:
             self._lockobj = Lock(self._lock_file)
         return self._lockobj
 
-    def new_uid(self):
-        """Return a new UID.
-
-        :return: The new uid
-        :rtype: int
-        """
+    def new(self):
+        """Return a new unique ID or a predictable one if in testing mode."""
         if layers.is_testing():
-            # When in testing mode we want to produce predictable id, but we
+            # When in testing mode we want to produce predictable ids, but we
             # need to coordinate this among separate processes.  We could use
             # the database, but I don't want to add schema just to handle this
             # case, and besides transactions could get aborted, causing some
@@ -78,17 +81,26 @@ class UniqueIDFactory:
             # may still not be ideal due to race conditions, but I think the
             # tests will be serialized enough (and the ids reset between
             # tests) that it will not be a problem.  Maybe.
-            return self._next_uid()
-        while True:
-            uid = uuid.uuid4()
-            try:
-                UID.record(uid)
-            except ValueError:
-                pass
-            else:
-                return uid
+            return self._next_predictable_id()
+        return self._next_unpredictable_id()
+
+    def _next_unpredictable_id(self):
+        """Generate a unique id when Mailman is not running in testing mode.
+
+        The type of the returned id is intended to be the type that
+        makes sense for the subclass overriding this method.
+        """
+        raise NotImplementedError
 
-    def _next_uid(self):
+    def _next_predictable_id(self):
+        """Generate a predictable id for when Mailman being tested.
+
+        The type of the returned id is intended to be the type that
+        makes sense for the subclass overriding this method.
+        """
+        raise NotImplementedError
+
+    def _next_id(self):
         with self._lock:
             try:
                 with open(self._uid_file) as fp:
@@ -96,13 +108,13 @@ class UniqueIDFactory:
                     next_uid = uid + 1
                 with open(self._uid_file, 'w') as fp:
                     fp.write(str(next_uid))
-                return uuid.UUID(int=uid)
+                return uid
             except IOError as error:
                 if error.errno != errno.ENOENT:
                     raise
                 with open(self._uid_file, 'w') as fp:
                     fp.write('2')
-                return uuid.UUID(int=1)
+                return 1
 
     def reset(self):
         with self._lock:
@@ -111,4 +123,50 @@ class UniqueIDFactory:
 
 
 
-factory = UniqueIDFactory()
+class UIDFactory(_PredictableIDGenerator):
+    """A factory for unique ids."""
+
+    def _next_unpredictable_id(self):
+        """Return a new UID.
+
+        :return: The new uid
+        :rtype: uuid.UUID
+        """
+        while True:
+            uid = uuid.uuid4()
+            try:
+                UID.record(uid)
+            except ValueError:
+                pass
+            else:
+                return uid
+
+    def _next_predictable_id(self):
+        uid = super()._next_id()
+        return uuid.UUID(int=uid)
+
+
+
+class TokenFactory(_PredictableIDGenerator):
+
+    def __init__(self):
+        super().__init__(context='token')
+
+    def _next_unpredictable_id(self):
+        """Calculate a unique token.
+
+        Algorithm vetted by the Timbot.  time() has high resolution on
+        Linux, clock() on Windows.  random gives us about 45 bits in
+        Python 2.2, 53 bits on Python 2.3.  The time and clock values
+        basically help obscure the random number generator, as does the
+        hash calculation.  The integral parts of the time values are
+        discarded because they're the most predictable bits.
+        """
+        right_now = time.time()
+        x = random.random() + right_now % 1.0 + time.clock() % 1.0
+        # Use sha1 because it produces shorter strings.
+        return hashlib.sha1(repr(x).encode('utf-8')).hexdigest()
+
+    def _next_predictable_id(self):
+        uid = super()._next_id()
+        return str(uid).zfill(40)



View it on GitLab: 
https://gitlab.com/mailman/mailman/compare/a11e089cc1e0e5aff2502e584014295a414a43f9...0e11f68b74beef848255272a8010cad3ea96af91
_______________________________________________
Mailman-checkins mailing list
Mailman-checkins@python.org
Unsubscribe: 
https://mail.python.org/mailman/options/mailman-checkins/archive%40jab.org

Reply via email to