GeezerVisonThe World Wide Web, restored to 800 pixels of glory. | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Home|About|Gaming cabinet|Archives|How it works|Starter sites | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
GeezerVison is showing github.com — gh-161452: Fix `python -m random` with a non-positive integer input by andresdelfino · Pull Request #161453 · python/cpython Conversation
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @@ -1076,6 +1075,8 @@ def main(arg_list: list[str] | None = None) -> int | str: | |||
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Don't you want to move this to an (inner) else: block too?
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
FWIW, I think it's the right place: uniform() not raises (c.f. randint).
So, here we have only parsing failures for floats. In that case it's reasonable to try choice() as a fallback.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
While Sergey is right, it would be easier to read if the same pattern is followed. Let's to think of. Let me know what you prefer.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
I agree with @gvanrossum, it's better to move return uniform(0, val) to an else: block as well.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Applied
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
This change is backward incompatible, but it respects the documentation: https://docs.python.org/dev/library/random.html#command-line-usage
If no options are given, the output depends on the input:
Integer: same as --integer.
Hum. Displaying a long traceback is not really user friendly. Can you catch the exception and display a (short) error message instead?
$ ./python -m random -3 Traceback (most recent call last): File "/home/vstinner/python/main/Lib/runpy.py", line 201, in _run_module_as_main return _run_code(code, main_globals, None, "__main__", mod_spec) File "/home/vstinner/python/main/Lib/runpy.py", line 87, in _run_code exec(code, run_globals) ~~~~^^^^^^^^^^^^^^^^^^^ File "/home/vstinner/python/main/Lib/random.py", line 1088, in <module> print(main()) ~~~~^^ File "/home/vstinner/python/main/Lib/random.py", line 1079, in main return randint(1, val) File "/home/vstinner/python/main/Lib/random.py", line 341, in randint raise ValueError(f"empty range in randint({a}, {b})") ValueError: empty range in randint(1, -3)
cc @hugovk who authored this feature.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Yes, a short error message would be better than the traceback. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
Move the uniform() call to an else block too, matching the integer branch. When run from the command line, catch the ValueError for an empty integer range and exit with a one-line error message instead of a traceback. Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
@vstinner I applied your suggestion, handling the exception in the block that calls main. Changing the main function itself would involve more changes in the tests. Would it be better to go that way? |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
|
Some general notes on
There is a surprising behavior when the integer is larger than sys.get_int_max_str_digits(). int() raises By the way, it's possible to pass special float values: It seems like Ok, maybe it can return And |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
Co-authored-by: Victor Stinner <[email protected]>
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
LGTM. I just have a last suggestion about adding a "real" random.main() function.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| import sys | ||
| try: | ||
| print(main()) | ||
| except ValueError as exc: | ||
| sys.exit(f"error: {exc}") |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
You might rename main() to _main() and wrap these lines into a new main() function. So for example, you can call the new main() function in tests instead of having to spawn a new process.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
What if I do like gzip instead, and call sys.exit directly from main? So we get the benefit you mention, and follow an existing pattern (personally I always find _main/main confusing).
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
What I suggest is to have a main() function which does everything: parse command line arguments, compute a random value, display the random value, call sys.exit() on error:
if __name__ == '__main__': main()
I don't care too much of how main() code would look like :-)
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Got it! We were talking about the same thing :) I'll implement this, as this seems to be the common way of implementing CLIs for modules.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
I ended up following your previous _main suggestion, as a lots of returns + a few try blocks would be necessary for main to handle everything properly by itself.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
Split the CLI logic into _main(), which returns the result, and make main() print it and exit with a short error message on failure. Test the error path in-process instead of spawning a subprocess. Co-Authored-By: Claude Opus 5.5 <[email protected]>
| self.assertEqual(str(cm.exception), | ||
| f"empty range in randint(1, {value})") | ||
|
|
||
| def test_main_error_no_traceback(self): |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
If test_empty_integer_range_error() is modified to test directly main(), I think that this test can be simply removed.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| instead of a traceback. :func:`!random.main` now prints the result instead | ||
| of returning it, and exits with the error message on failure. |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Since we don't document the random.main() function, I don't think that we should document when its behavior changes.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| ]: | ||
| random.seed(0) | ||
| self.assertEqual(random.main(shlex.split(command)), expected) | ||
| self.assertEqual(random._main(shlex.split(command)), expected) |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Same here, I would prefer to test the public main() function:
| self.assertEqual(random._main(shlex.split(command)), expected) | |
| with support.captured_stdout() as stdout: | |
| random.main(shlex.split(command)) | |
| self.assertEqual(stdout.getvalue().rstrip(), str(expected)) |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| for value in ["0", "-5"]: | ||
| for args in [[value], ["--integer", value]]: | ||
| with self.subTest(args=args): | ||
| with self.assertRaises(ValueError) as cm: | ||
| random._main(args) | ||
| self.assertEqual(str(cm.exception), | ||
| f"empty range in randint(1, {value})") |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
I would prefer testing the public main() function, rather than testing _main() which is more an implementation detail:
| for value in ["0", "-5"]: | |
| for args in [[value], ["--integer", value]]: | |
| with self.subTest(args=args): | |
| with self.assertRaises(ValueError) as cm: | |
| random._main(args) | |
| self.assertEqual(str(cm.exception), | |
| f"empty range in randint(1, {value})") | |
| for value in ["0", "-5"]: | |
| err_msg = f"error: empty range in randint(1, {value})" | |
| for args in [[value], ["--integer", value]]: | |
| with self.subTest(args=args): | |
| with self.assertRaises(SystemExit) as cm: | |
| random.main(args) | |
| self.assertEqual(str(cm.exception), err_msg) |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
I'm also not keen on The traceback is now replaced with a short error: ❯ ./python.exe -m random -3 error: empty range in randint(1, -3) But that doesn't mean much to me. What's a randint? Before I had a traceback pointing me to where it all came from so I could look it up. So I suggest, let's keep: if __name__ == '__main__': print(main()) Add this validation and call def _randint_cli(parser, n: int) -> int: if n < 1: parser.error(f"integer must be 1 or greater, got {n}") return randint(1, n) This mean we get the native argparse error, which prints usage, and also we get colour (and colour handling): And |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
Keep main() returning its result as before, and have the CLI reject a non-positive integer through argparse's parser.error(), so it prints the usage and a short error message and exits with status 2. Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
@hugovk I followed your suggestions |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Thanks!
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| args, parser = random._parse_args(shlex.split("--choice a b c")) | ||
| self.assertEqual(args.choice, ["a", "b", "c"]) | ||
| self.assertStartsWith(help_text, "usage: ") | ||
| self.assertStartsWith(parser.format_help(), "usage: ") |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
We could now remove these lines, we don't need to test argparse.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Just pushed the change. Let me know if I understood you correctly.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
Successfully merging this pull request may close these issues.
There was an error while loading. Please reload this page.