Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-118131: Command-line interface for the random module #118132

Merged
merged 4 commits into from
May 5, 2024
Merged
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Doc/library/cmdline.rst
Original file line number Diff line number Diff line change
@@ -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>`
80 changes: 80 additions & 0 deletions Doc/library/random.rst
Original file line number Diff line number Diff line change
@@ -700,3 +700,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
@@ -651,6 +651,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.
72 changes: 71 additions & 1 deletion Lib/random.py
Original file line number Diff line number Diff line change
@@ -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)
Copy link
Contributor

@andersk andersk Sep 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In #118131 this was specced for floats to use the range between 0 and N of length N, which would be more useful and makes a lot more sense than the range between 1 and N of length N - 1.


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
@@ -4,6 +4,7 @@
import os
import time
import pickle
import shlex
import warnings
import test.support

@@ -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.