From c191b00b584720e87fa0b1beeb8d302924bb5a68 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 24 Apr 2020 11:20:09 +0200 Subject: [PATCH 1/7] bpo-40346: Random subclass randbytes uses getrandbits Subclasses of the random.Random and random.SystemRandom classes now get a randbytes() method implementation which uses the getrandbits() method. --- Doc/library/random.rst | 2 ++ Lib/random.py | 20 +++++++++++++++++++ Lib/test/test_random.py | 14 +++++++++++++ .../2020-04-24-11-22-10.bpo-40346.viRmGr.rst | 3 +++ 4 files changed, 39 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2020-04-24-11-22-10.bpo-40346.viRmGr.rst diff --git a/Doc/library/random.rst b/Doc/library/random.rst index 291eca3a3f16a1..e5564693d65948 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. 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..0302a06bb14f67 100644 --- a/Lib/random.py +++ b/Lib/random.py @@ -120,6 +120,11 @@ def __init_subclass__(cls, /, **kwargs): cls._randbelow = cls._randbelow_without_getrandbits break + if cls.randbytes == _random.Random.randbytes: + # Subclasses of random.Random implement randbytes() + # using getrandbits() + cls.randbytes = cls._randbytes_getrandbits + def seed(self, a=None, version=2): """Initialize internal state from a seed. @@ -189,6 +194,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 +744,14 @@ 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: + # Subclasses of random.SystemRandom implement randbytes() + # using getrandbits() + 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..b5a3c284cb1e77 100644 --- a/Lib/test/test_random.py +++ b/Lib/test/test_random.py @@ -335,6 +335,20 @@ 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() + class Subclass(type(self.gen)): + calls = [] + def getrandbits(self, n): + self.calls.append(n) + return 0 + + subclass = Subclass() + for n in range(10): + self.assertEqual(subclass.randbytes(n), b'\0' * n) + self.assertEqual(subclass.calls, [n * 8 for n in range(10)]) + 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. From 5c9af5f37f2bda7bae56b9cfbb985e22e0dd3e6e Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 24 Apr 2020 14:11:38 +0200 Subject: [PATCH 2/7] Add more tests on subclassing --- Doc/library/random.rst | 2 +- Lib/test/test_random.py | 44 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/Doc/library/random.rst b/Doc/library/random.rst index e5564693d65948..b75bd917229744 100644 --- a/Doc/library/random.rst +++ b/Doc/library/random.rst @@ -39,7 +39,7 @@ basic generator of your own devising: in that case, override the :meth:`~Random. 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. +: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/test/test_random.py b/Lib/test/test_random.py index b5a3c284cb1e77..c7a134856ded71 100644 --- a/Lib/test/test_random.py +++ b/Lib/test/test_random.py @@ -339,15 +339,53 @@ def test_randbytes_subclass(self): # For random.Random and random.SystemRandom subclasses, # randbytes() is implemented with getrandbits() class Subclass(type(self.gen)): - calls = [] + getrandbits_calls = [] def getrandbits(self, n): - self.calls.append(n) + self.getrandbits_calls.append(n) return 0 + class SubSubclass(Subclass): + randbytes_calls = [] + def randbytes(self, n): + self.randbytes_calls.append(n) + return b'\x04' * n + + # SubSubclass override explicitly randbytes(): + # getrandbits() implemented in Subclass is not called. + # + # Test SubSubclass first, since it shares randbytes_calls + # and getrandbits_calls lists with Subclass. + 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, []) + + # Subclass implements getrandbits() but not randbytes() subclass = Subclass() for n in range(10): self.assertEqual(subclass.randbytes(n), b'\0' * n) - self.assertEqual(subclass.calls, [n * 8 for n in range(10)]) + self.assertEqual(subclass.getrandbits_calls, + [n * 8 for n in range(10)]) + + class Subclass2(type(self.gen)): + 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'\x04' * n + + # Subclass implements getrandbits() and randbytes(): + # randbytes() doesn't use getrandbits(). + subclass2 = Subclass2() + for n in range(10): + self.assertEqual(subclass2.randbytes(n), b'\x04' * n) + self.assertEqual(subclass2.randbytes_calls, list(range(10))) + self.assertEqual(subclass2.getrandbits_calls, []) try: From 746a83c6df03490160b2dfa022ad91dbcff6a0f4 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 24 Apr 2020 14:16:23 +0200 Subject: [PATCH 3/7] Only use _randbytes_getrandbits if getrandbits() is overridden --- Lib/random.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Lib/random.py b/Lib/random.py index 0302a06bb14f67..7d273e48e32b3d 100644 --- a/Lib/random.py +++ b/Lib/random.py @@ -120,9 +120,10 @@ def __init_subclass__(cls, /, **kwargs): cls._randbelow = cls._randbelow_without_getrandbits break - if cls.randbytes == _random.Random.randbytes: + if (cls.randbytes == _random.Random.randbytes + and cls.getrandbits != _random.Random.getrandbits): # Subclasses of random.Random implement randbytes() - # using getrandbits() + # using getrandbits() if getrandbits() is overriden. cls.randbytes = cls._randbytes_getrandbits def seed(self, a=None, version=2): @@ -747,9 +748,10 @@ class SystemRandom(Random): def __init_subclass__(cls, /, **kwargs): super.__init_subclass__(**kwargs) - if cls.randbytes == SystemRandom.randbytes: + if (cls.randbytes == SystemRandom.randbytes + and cls.getrandbits != _random.Random.getrandbits): # Subclasses of random.SystemRandom implement randbytes() - # using getrandbits() + # using getrandbits() if getrandbits() is overriden. cls.randbytes = cls._randbytes_getrandbits def random(self): From 584737e5ae9c7157a74a0bd07146323e93bd45a2 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 24 Apr 2020 14:19:05 +0200 Subject: [PATCH 4/7] Fix randbytes() __name__ for subclasses --- Lib/random.py | 6 +++++- Lib/test/test_random.py | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Lib/random.py b/Lib/random.py index 7d273e48e32b3d..2faf1dabf97858 100644 --- a/Lib/random.py +++ b/Lib/random.py @@ -198,9 +198,13 @@ def setstate(self, state): ## -------------------- bytes ----------------------------- # Implementation used by Random and SystemRandom subclasses - def _randbytes_getrandbits(self, n): + def randbytes(self, n): """Generate n random bytes.""" return self.getrandbits(n * 8).to_bytes(n, 'little') + # Declare the function as "randbytes" to set __name__ and __qualname__ + # to "randbytes" + _randbytes_getrandbits = randbytes + del randbytes ## ---- Methods below this point do not need to be overridden when ## ---- subclassing for the purpose of using a different core generator. diff --git a/Lib/test/test_random.py b/Lib/test/test_random.py index c7a134856ded71..0d73a9a8d9f6de 100644 --- a/Lib/test/test_random.py +++ b/Lib/test/test_random.py @@ -368,6 +368,10 @@ def randbytes(self, n): self.assertEqual(subclass.getrandbits_calls, [n * 8 for n in range(10)]) + # Check the method name in a subclass + self.assertEqual(subclass.randbytes.__name__, "randbytes") + self.assertEqual(subclass.randbytes.__qualname__, "Random.randbytes") + class Subclass2(type(self.gen)): getrandbits_calls = [] def getrandbits(self, n): From 9820a8a9546fec4c18ef2e5751b4650e895bb66a Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 24 Apr 2020 17:09:25 +0200 Subject: [PATCH 5/7] Fix SystemRandom.__init_subclass__() super.__init_subclass__ => super().__init_subclass__ Cleanup also tests. --- Lib/random.py | 2 +- Lib/test/test_random.py | 52 +++++++++++++++++++++++------------------ 2 files changed, 30 insertions(+), 24 deletions(-) diff --git a/Lib/random.py b/Lib/random.py index 2faf1dabf97858..c8631fa08bd3a7 100644 --- a/Lib/random.py +++ b/Lib/random.py @@ -750,7 +750,7 @@ class SystemRandom(Random): """ def __init_subclass__(cls, /, **kwargs): - super.__init_subclass__(**kwargs) + super().__init_subclass__(**kwargs) if (cls.randbytes == SystemRandom.randbytes and cls.getrandbits != _random.Random.getrandbits): diff --git a/Lib/test/test_random.py b/Lib/test/test_random.py index 0d73a9a8d9f6de..6664169716f00f 100644 --- a/Lib/test/test_random.py +++ b/Lib/test/test_random.py @@ -338,42 +338,50 @@ def test_randbytes(self): 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)): - getrandbits_calls = [] + 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)]) + + self.assertEqual(subclass.randbytes.__name__, "randbytes") + self.assertEqual(subclass.randbytes.__qualname__, "Random.randbytes") + + # SubSubclass override explicitly randbytes(): + # getrandbits() implemented in Subclass is not called. class SubSubclass(Subclass): - randbytes_calls = [] + def __init__(self): + super().__init__() + self.randbytes_calls = [] + def randbytes(self, n): self.randbytes_calls.append(n) return b'\x04' * n - # SubSubclass override explicitly randbytes(): - # getrandbits() implemented in Subclass is not called. - # - # Test SubSubclass first, since it shares randbytes_calls - # and getrandbits_calls lists with Subclass. 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, []) - # Subclass implements getrandbits() but not randbytes() - 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)]) - - # Check the method name in a subclass - self.assertEqual(subclass.randbytes.__name__, "randbytes") - self.assertEqual(subclass.randbytes.__qualname__, "Random.randbytes") - + # Subclass2 implements getrandbits() and randbytes(): + # randbytes() doesn't use getrandbits(). class Subclass2(type(self.gen)): - getrandbits_calls = [] + def __init__(self): + super().__init__() + self.getrandbits_calls = [] + def getrandbits(self, n): self.getrandbits_calls.append(n) return 0 @@ -381,13 +389,11 @@ def getrandbits(self, n): randbytes_calls = [] def randbytes(self, n): self.randbytes_calls.append(n) - return b'\x04' * n + return b'\x07' * n - # Subclass implements getrandbits() and randbytes(): - # randbytes() doesn't use getrandbits(). subclass2 = Subclass2() for n in range(10): - self.assertEqual(subclass2.randbytes(n), b'\x04' * n) + self.assertEqual(subclass2.randbytes(n), b'\x07' * n) self.assertEqual(subclass2.randbytes_calls, list(range(10))) self.assertEqual(subclass2.getrandbits_calls, []) From 2d97c8fa37afa1792efc022463d072b49ff7ffdc Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 24 Apr 2020 17:11:08 +0200 Subject: [PATCH 6/7] Fix SystemRandom.__init_subclass__() Check SystemRandom.getrandbits, not _random.Random.getrandbits. --- Lib/random.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/random.py b/Lib/random.py index c8631fa08bd3a7..c98be7b322a889 100644 --- a/Lib/random.py +++ b/Lib/random.py @@ -753,7 +753,7 @@ def __init_subclass__(cls, /, **kwargs): super().__init_subclass__(**kwargs) if (cls.randbytes == SystemRandom.randbytes - and cls.getrandbits != _random.Random.getrandbits): + and cls.getrandbits != SystemRandom.getrandbits): # Subclasses of random.SystemRandom implement randbytes() # using getrandbits() if getrandbits() is overriden. cls.randbytes = cls._randbytes_getrandbits From 5d519e04ea6fb73be5fb0881cc24dfbd08c3d5b0 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 24 Apr 2020 17:57:03 +0200 Subject: [PATCH 7/7] Remove __name__/__qualname__ name which breaks pickle --- Lib/random.py | 6 +----- Lib/test/test_random.py | 3 --- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/Lib/random.py b/Lib/random.py index c98be7b322a889..3ddff7cc446b18 100644 --- a/Lib/random.py +++ b/Lib/random.py @@ -198,13 +198,9 @@ def setstate(self, state): ## -------------------- bytes ----------------------------- # Implementation used by Random and SystemRandom subclasses - def randbytes(self, n): + def _randbytes_getrandbits (self, n): """Generate n random bytes.""" return self.getrandbits(n * 8).to_bytes(n, 'little') - # Declare the function as "randbytes" to set __name__ and __qualname__ - # to "randbytes" - _randbytes_getrandbits = randbytes - del randbytes ## ---- Methods below this point do not need to be overridden when ## ---- subclassing for the purpose of using a different core generator. diff --git a/Lib/test/test_random.py b/Lib/test/test_random.py index 6664169716f00f..4785eb8f6b3ade 100644 --- a/Lib/test/test_random.py +++ b/Lib/test/test_random.py @@ -355,9 +355,6 @@ def getrandbits(self, n): self.assertEqual(subclass.getrandbits_calls, [n * 8 for n in range(10)]) - self.assertEqual(subclass.randbytes.__name__, "randbytes") - self.assertEqual(subclass.randbytes.__qualname__, "Random.randbytes") - # SubSubclass override explicitly randbytes(): # getrandbits() implemented in Subclass is not called. class SubSubclass(Subclass):