Source code for toy_crypto.rand

import math
import secrets
from bisect import bisect
from collections.abc import MutableSequence, Sequence
from itertools import accumulate, repeat
from typing import Any, Optional


[docs] def randrange(*args: int) -> int: """ Like :py:func:`random.randrange`, but uses RNG from :py:mod:`secrets`. """ if any([not isinstance(arg, int) for arg in args]): raise TypeError("arguments must be integers") start = 0 step = 1 match len(args): case 1: stop = args[0] case 2: start = args[0] stop = args[1] case 3: start = args[0] stop = args[1] step = args[2] case _: raise TypeError("Must have 1, 2, or 3 arguments") diff = stop - start if diff < 1: raise ValueError("stop must be greater than start") if step < 1: raise ValueError("step must be positive") if diff == 1: return start if step >= diff: # only the bottom of the range will be allowed return start r = secrets.randbelow(diff // step) r *= step r += start return r
[docs] def shuffle(x: MutableSequence[Any]) -> None: """Like :py:func:`random.shuffle`, but uses RNG from :py:mod:`secrets`.""" # Uses the "modern" Fisher-Yates shuffle from Knuth via # https://en.wikipedia.org/wiki/Fisher–Yates_shuffle#The_modern_algorithm n = len(x) if n < 2: return for i in range(n - 1): j = randrange(i, n) x[i], x[j] = x[j], x[i]
# from FullRandom example in # https://docs.python.org/3/library/random.html#examples
[docs] def random() -> float: """returns a 32-bit float in [0.0, 1.0)""" mantissa = 0x10_0000_0000_0000 | secrets.randbits(52) exponent = -53 x = 0 while not x: x = secrets.randbits(32) exponent += x.bit_length() - 32 return math.ldexp(mantissa, exponent)
[docs] def choices[T]( population: Sequence[T], weights: Optional[Sequence[float]] = None, *, cum_weights: Optional[Sequence[float]] = None, k: int = 1, ) -> Sequence[T]: """Return a k sized list of population elements chosen with replacement. If the relative weights or cumulative weights are not specified, the selections are made with equal probability. This is the same as :py:func:`random.choices` except that it uses the the random number generator from :py:mod:1secrets`. Indeed, the implementation is just copied from that source. """ # copied from https://github.com/python/cpython/blob/3.13/Lib/random.py#L458 # except with static typing # and less abstract naming conventions for imported things n = len(population) if cum_weights is None: if weights is None: floor = math.floor return [population[floor(random() * n)] for i in repeat(None, k)] try: cum_weights = list(accumulate(weights)) except TypeError: if not isinstance(weights, int): raise k = weights raise TypeError( f"The number of choices must be a keyword argument: {k=}" ) from None elif weights is not None: raise TypeError("Cannot specify both weights and cumulative weights") if len(cum_weights) != n: raise ValueError("The number of weights does not match the population") total = float(cum_weights[-1]) if total <= 0.0: raise ValueError("Total of weights must be greater than zero") if not math.isfinite(total): raise ValueError("Total of weights must be finite") hi = n - 1 return [ population[bisect(cum_weights, random() * total, 0, hi)] for i in repeat(None, k) ]