From 5fc54493b6f71a5339a7f12a42e9ed32c9cc2320 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 17 Apr 2024 17:50:52 +0300 Subject: [PATCH 1/3] Add command-line interface for the `random` module --- Doc/library/cmdline.rst | 1 + Doc/library/random.rst | 84 +++++++++++++++++++++++++++++++++++++++++ Doc/whatsnew/3.13.rst | 6 +++ Lib/random.py | 72 ++++++++++++++++++++++++++++++++++- Lib/test/test_random.py | 43 +++++++++++++++++++++ 5 files changed, 205 insertions(+), 1 deletion(-) diff --git a/Doc/library/cmdline.rst b/Doc/library/cmdline.rst index b2379befeffcba..5174515ffc23ed 100644 --- a/Doc/library/cmdline.rst +++ b/Doc/library/cmdline.rst @@ -36,6 +36,7 @@ The following modules have a command-line interface. * :mod:`pyclbr` * :mod:`pydoc` * :mod:`quopri` +* :ref:`random ` * :mod:`runpy` * :ref:`site ` * :ref:`sqlite3 ` diff --git a/Doc/library/random.rst b/Doc/library/random.rst index 8fbce18c56f17c..b06142a68effc2 100644 --- a/Doc/library/random.rst +++ b/Doc/library/random.rst @@ -700,3 +700,87 @@ positive unnormalized float and is equal to ``math.ulp(0.0)``.) `_ 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 | --test [N]] + +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 + --integer + + Print a random integer between 1 and N inclusive, using :meth:`randint`. + +.. option:: -f + --float + + Print a random floating point number between 1 and N inclusive, + using :meth:`uniform`. + +.. option:: --test + + Run a test N times. + +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 diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index 5be562030b507b..cbf882d8bd2040 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -651,6 +651,12 @@ queue termination. (Contributed by Laurie Opperman and Yves Duprat in :gh:`104750`.) +random +------ + +* Add a :ref:`command-line interface `. + (Contributed by Hugo van Kemenade in :gh:`54321`.) + re -- * Rename :exc:`!re.error` to :exc:`re.PatternError` for improved clarity. diff --git a/Lib/random.py b/Lib/random.py index 875beb2f8cf41c..30ac80b77e07cf 100644 --- a/Lib/random.py +++ b/Lib/random.py @@ -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()) diff --git a/Lib/test/test_random.py b/Lib/test/test_random.py index b1e4ef4197d130..589d1243a4cac1 100644 --- a/Lib/test/test_random.py +++ b/Lib/test/test_random.py @@ -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() From 5a487bb75d451ae9ff6094b18aa168114d30462c Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 21 Apr 2024 19:05:59 +0300 Subject: [PATCH 2/3] Add blurb, update docs --- Doc/library/random.rst | 6 +----- .../Library/2024-04-21-18-55-42.gh-issue-118131.eAT0is.rst | 2 ++ 2 files changed, 3 insertions(+), 5 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2024-04-21-18-55-42.gh-issue-118131.eAT0is.rst diff --git a/Doc/library/random.rst b/Doc/library/random.rst index b06142a68effc2..b3856c5aa44a20 100644 --- a/Doc/library/random.rst +++ b/Doc/library/random.rst @@ -712,7 +712,7 @@ 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 | --test [N]] + python -m random [-h] [-c CHOICE [CHOICE ...] | -i N | -f N] [input ...] The following options are accepted: @@ -738,10 +738,6 @@ The following options are accepted: Print a random floating point number between 1 and N inclusive, using :meth:`uniform`. -.. option:: --test - - Run a test N times. - If no options are given, the output depends on the input: * String or multiple: same as :option:`--choice`. diff --git a/Misc/NEWS.d/next/Library/2024-04-21-18-55-42.gh-issue-118131.eAT0is.rst b/Misc/NEWS.d/next/Library/2024-04-21-18-55-42.gh-issue-118131.eAT0is.rst new file mode 100644 index 00000000000000..83ed66cf82fc20 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-04-21-18-55-42.gh-issue-118131.eAT0is.rst @@ -0,0 +1,2 @@ +Add command-line interface for the :mod:`random` module. Patch by Hugo van +Kemenade. From 15dec310aebbd4907e8dda1838d26938637f6f77 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 2 May 2024 16:37:02 +0300 Subject: [PATCH 3/3] Rename parse_args to _parse_args --- Lib/random.py | 4 ++-- Lib/test/test_random.py | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Lib/random.py b/Lib/random.py index 30ac80b77e07cf..bcc11c7cd3c208 100644 --- a/Lib/random.py +++ b/Lib/random.py @@ -1000,7 +1000,7 @@ def _test(N=10_000): # -------------- command-line interface ---------------- -def parse_args(arg_list: list[str] | None): +def _parse_args(arg_list: list[str] | None): import argparse parser = argparse.ArgumentParser( formatter_class=argparse.RawTextHelpFormatter) @@ -1028,7 +1028,7 @@ def parse_args(arg_list: list[str] | None): def main(arg_list: list[str] | None = None) -> int | str: - args, help_text = parse_args(arg_list) + args, help_text = _parse_args(arg_list) # Explicit arguments if args.choice: diff --git a/Lib/test/test_random.py b/Lib/test/test_random.py index 589d1243a4cac1..9a44ab1768656a 100644 --- a/Lib/test/test_random.py +++ b/Lib/test/test_random.py @@ -1400,27 +1400,27 @@ def test_after_fork(self): class CommandLineTest(unittest.TestCase): def test_parse_args(self): - args, help_text = random.parse_args(shlex.split("--choice a b c")) + 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")) + 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")) + 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")) + 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")) + 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")) + args, help_text = random._parse_args(shlex.split("2.5")) self.assertEqual(args.input, ["2.5"]) self.assertTrue(help_text.startswith("usage: "))