Skip to content

Commit

Permalink
gh-118131: Command-line interface for the random module (#118132)
Browse files Browse the repository at this point in the history
  • Loading branch information
hugovk authored May 5, 2024
1 parent fed8d73 commit 3b32575
Show file tree
Hide file tree
Showing 6 changed files with 203 additions and 1 deletion.
1 change: 1 addition & 0 deletions Doc/library/cmdline.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ The following modules have a command-line interface.
* :mod:`pyclbr`
* :mod:`pydoc`
* :mod:`quopri`
* :ref:`random <random-cli>`
* :mod:`runpy`
* :ref:`site <site-commandline>`
* :ref:`sqlite3 <sqlite3-cli>`
Expand Down
80 changes: 80 additions & 0 deletions Doc/library/random.rst
Original file line number Diff line number Diff line change
Expand Up @@ -706,3 +706,83 @@ positive unnormalized float and is equal to ``math.ulp(0.0)``.)
<https://allendowney.com/research/rand/downey07randfloat.pdf>`_ a
paper by Allen B. Downey describing ways to generate more
fine-grained floats than normally generated by :func:`.random`.

.. _random-cli:

Command-line usage
------------------

.. versionadded:: 3.13

The :mod:`!random` module can be executed from the command line.

.. code-block:: sh
python -m random [-h] [-c CHOICE [CHOICE ...] | -i N | -f N] [input ...]
The following options are accepted:

.. program:: random

.. option:: -h, --help

Show the help message and exit.

.. option:: -c CHOICE [CHOICE ...]
--choice CHOICE [CHOICE ...]

Print a random choice, using :meth:`choice`.

.. option:: -i <N>
--integer <N>

Print a random integer between 1 and N inclusive, using :meth:`randint`.

.. option:: -f <N>
--float <N>

Print a random floating point number between 1 and N inclusive,
using :meth:`uniform`.

If no options are given, the output depends on the input:

* String or multiple: same as :option:`--choice`.
* Integer: same as :option:`--integer`.
* Float: same as :option:`--float`.

.. _random-cli-example:

Command-line example
--------------------

Here are some examples of the :mod:`!random` command-line interface:

.. code-block:: console
$ # Choose one at random
$ python -m random egg bacon sausage spam "Lobster Thermidor aux crevettes with a Mornay sauce"
Lobster Thermidor aux crevettes with a Mornay sauce
$ # Random integer
$ python -m random 6
6
$ # Random floating-point number
$ python -m random 1.8
1.7080016272295635
$ # With explicit arguments
$ python -m random --choice egg bacon sausage spam "Lobster Thermidor aux crevettes with a Mornay sauce"
egg
$ python -m random --integer 6
3
$ python -m random --float 1.8
1.5666339105010318
$ python -m random --integer 6
5
$ python -m random --float 6
3.1942323316565915
6 changes: 6 additions & 0 deletions Doc/whatsnew/3.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,12 @@ queue
termination.
(Contributed by Laurie Opperman and Yves Duprat in :gh:`104750`.)

random
------

* Add a :ref:`command-line interface <random-cli>`.
(Contributed by Hugo van Kemenade in :gh:`54321`.)

re
--
* Rename :exc:`!re.error` to :exc:`re.PatternError` for improved clarity.
Expand Down
72 changes: 71 additions & 1 deletion Lib/random.py
Original file line number Diff line number Diff line change
Expand Up @@ -996,5 +996,75 @@ def _test(N=10_000):
_os.register_at_fork(after_in_child=_inst.seed)


# ------------------------------------------------------
# -------------- command-line interface ----------------


def _parse_args(arg_list: list[str] | None):
import argparse
parser = argparse.ArgumentParser(
formatter_class=argparse.RawTextHelpFormatter)
group = parser.add_mutually_exclusive_group()
group.add_argument(
"-c", "--choice", nargs="+",
help="print a random choice")
group.add_argument(
"-i", "--integer", type=int, metavar="N",
help="print a random integer between 1 and N inclusive")
group.add_argument(
"-f", "--float", type=float, metavar="N",
help="print a random floating point number between 1 and N inclusive")
group.add_argument(
"--test", type=int, const=10_000, nargs="?",
help=argparse.SUPPRESS)
parser.add_argument("input", nargs="*",
help="""\
if no options given, output depends on the input
string or multiple: same as --choice
integer: same as --integer
float: same as --float""")
args = parser.parse_args(arg_list)
return args, parser.format_help()


def main(arg_list: list[str] | None = None) -> int | str:
args, help_text = _parse_args(arg_list)

# Explicit arguments
if args.choice:
return choice(args.choice)

if args.integer is not None:
return randint(1, args.integer)

if args.float is not None:
return uniform(1, args.float)

if args.test:
_test(args.test)
return ""

# No explicit argument, select based on input
if len(args.input) == 1:
val = args.input[0]
try:
# Is it an integer?
val = int(val)
return randint(1, val)
except ValueError:
try:
# Is it a float?
val = float(val)
return uniform(1, val)
except ValueError:
# Split in case of space-separated string: "a b c"
return choice(val.split())

if len(args.input) >= 2:
return choice(args.input)

return help_text


if __name__ == '__main__':
_test()
print(main())
43 changes: 43 additions & 0 deletions Lib/test/test_random.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import os
import time
import pickle
import shlex
import warnings
import test.support

Expand Down Expand Up @@ -1397,5 +1398,47 @@ def test_after_fork(self):
support.wait_process(pid, exitcode=0)


class CommandLineTest(unittest.TestCase):
def test_parse_args(self):
args, help_text = random._parse_args(shlex.split("--choice a b c"))
self.assertEqual(args.choice, ["a", "b", "c"])
self.assertTrue(help_text.startswith("usage: "))

args, help_text = random._parse_args(shlex.split("--integer 5"))
self.assertEqual(args.integer, 5)
self.assertTrue(help_text.startswith("usage: "))

args, help_text = random._parse_args(shlex.split("--float 2.5"))
self.assertEqual(args.float, 2.5)
self.assertTrue(help_text.startswith("usage: "))

args, help_text = random._parse_args(shlex.split("a b c"))
self.assertEqual(args.input, ["a", "b", "c"])
self.assertTrue(help_text.startswith("usage: "))

args, help_text = random._parse_args(shlex.split("5"))
self.assertEqual(args.input, ["5"])
self.assertTrue(help_text.startswith("usage: "))

args, help_text = random._parse_args(shlex.split("2.5"))
self.assertEqual(args.input, ["2.5"])
self.assertTrue(help_text.startswith("usage: "))

def test_main(self):
for command, expected in [
("--choice a b c", "b"),
('"a b c"', "b"),
("a b c", "b"),
("--choice 'a a' 'b b' 'c c'", "b b"),
("'a a' 'b b' 'c c'", "b b"),
("--integer 5", 4),
("5", 4),
("--float 2.5", 2.266632777287572),
("2.5", 2.266632777287572),
]:
random.seed(0)
self.assertEqual(random.main(shlex.split(command)), expected)


if __name__ == "__main__":
unittest.main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add command-line interface for the :mod:`random` module. Patch by Hugo van
Kemenade.

0 comments on commit 3b32575

Please sign in to comment.