diff --git a/Doc/library/random.rst b/Doc/library/random.rst index 291eca3a3f16a1..b75bd917229744 100644 --- a/Doc/library/random.rst +++ b/Doc/library/random.rst @@ -38,6 +38,8 @@ basic generator of your own devising: in that case, override the :meth:`~Random. :meth:`~Random.seed`, :meth:`~Random.getstate`, and :meth:`~Random.setstate` methods. Optionally, a new generator can supply a :meth:`~Random.getrandbits` method --- this allows :meth:`randrange` to produce selections over an arbitrarily large range. +In subclasses, :meth:`randbytes` is implemented with the +:meth:`~Random.getrandbits` method, unless :meth:`randbytes` is overriden. The :mod:`random` module also provides the :class:`SystemRandom` class which uses the system function :func:`os.urandom` to generate random numbers diff --git a/Lib/random.py b/Lib/random.py index f1df18d5c187b8..3ddff7cc446b18 100644 --- a/Lib/random.py +++ b/Lib/random.py @@ -120,6 +120,12 @@ def __init_subclass__(cls, /, **kwargs): cls._randbelow = cls._randbelow_without_getrandbits break + if (cls.randbytes == _random.Random.randbytes + and cls.getrandbits != _random.Random.getrandbits): + # Subclasses of random.Random implement randbytes() + # using getrandbits() if getrandbits() is overriden. + cls.randbytes = cls._randbytes_getrandbits + def seed(self, a=None, version=2): """Initialize internal state from a seed. @@ -189,6 +195,13 @@ def setstate(self, state): "Random.setstate() of version %s" % (version, self.VERSION)) +## -------------------- bytes ----------------------------- + + # Implementation used by Random and SystemRandom subclasses + def _randbytes_getrandbits (self, n): + """Generate n random bytes.""" + return self.getrandbits(n * 8).to_bytes(n, 'little') + ## ---- Methods below this point do not need to be overridden when ## ---- subclassing for the purpose of using a different core generator. @@ -732,6 +745,15 @@ class SystemRandom(Random): Not available on all systems (see os.urandom() for details). """ + def __init_subclass__(cls, /, **kwargs): + super().__init_subclass__(**kwargs) + + if (cls.randbytes == SystemRandom.randbytes + and cls.getrandbits != SystemRandom.getrandbits): + # Subclasses of random.SystemRandom implement randbytes() + # using getrandbits() if getrandbits() is overriden. + cls.randbytes = cls._randbytes_getrandbits + def random(self): """Get the next random number in the range [0.0, 1.0).""" return (int.from_bytes(_urandom(7), 'big') >> 3) * RECIP_BPF diff --git a/Lib/test/test_random.py b/Lib/test/test_random.py index 42c68dd1c24422..4785eb8f6b3ade 100644 --- a/Lib/test/test_random.py +++ b/Lib/test/test_random.py @@ -335,6 +335,65 @@ def test_randbytes(self): self.assertRaises(ValueError, self.gen.randbytes, -1) self.assertRaises(TypeError, self.gen.randbytes, 1.0) + def test_randbytes_subclass(self): + # For random.Random and random.SystemRandom subclasses, + # randbytes() is implemented with getrandbits() + + # Subclass implements getrandbits() but not randbytes() + class Subclass(type(self.gen)): + def __init__(self): + super().__init__() + self.getrandbits_calls = [] + + def getrandbits(self, n): + self.getrandbits_calls.append(n) + return 0 + + subclass = Subclass() + for n in range(10): + self.assertEqual(subclass.randbytes(n), b'\0' * n) + self.assertEqual(subclass.getrandbits_calls, + [n * 8 for n in range(10)]) + + # SubSubclass override explicitly randbytes(): + # getrandbits() implemented in Subclass is not called. + class SubSubclass(Subclass): + def __init__(self): + super().__init__() + self.randbytes_calls = [] + + def randbytes(self, n): + self.randbytes_calls.append(n) + return b'\x04' * n + + subsubclass = SubSubclass() + for n in range(10): + self.assertEqual(subsubclass.randbytes(n), b'\x04' * n) + self.assertEqual(subsubclass.randbytes_calls, list(range(10))) + self.assertEqual(subsubclass.getrandbits_calls, []) + + # Subclass2 implements getrandbits() and randbytes(): + # randbytes() doesn't use getrandbits(). + class Subclass2(type(self.gen)): + def __init__(self): + super().__init__() + self.getrandbits_calls = [] + + def getrandbits(self, n): + self.getrandbits_calls.append(n) + return 0 + + randbytes_calls = [] + def randbytes(self, n): + self.randbytes_calls.append(n) + return b'\x07' * n + + subclass2 = Subclass2() + for n in range(10): + self.assertEqual(subclass2.randbytes(n), b'\x07' * n) + self.assertEqual(subclass2.randbytes_calls, list(range(10))) + self.assertEqual(subclass2.getrandbits_calls, []) + try: random.SystemRandom().random() diff --git a/Misc/NEWS.d/next/Library/2020-04-24-11-22-10.bpo-40346.viRmGr.rst b/Misc/NEWS.d/next/Library/2020-04-24-11-22-10.bpo-40346.viRmGr.rst new file mode 100644 index 00000000000000..b323c1c4e87c61 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2020-04-24-11-22-10.bpo-40346.viRmGr.rst @@ -0,0 +1,3 @@ +Subclasses of the :class:`random.Random` and :class:`random.SystemRandom` +classes now get a ``randbytes()`` method implementation which uses the +``getrandbits()`` method.