From 5eed43f21f2ad801d65c7db266ff902788a5e30a Mon Sep 17 00:00:00 2001 From: Harry Lees Date: Fri, 21 Mar 2025 12:52:06 +0000 Subject: [PATCH 01/16] Convert platform CLI to use argparse --- Lib/platform.py | 27 ++++++++++++++++----- Lib/test/test_platform.py | 51 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 6 deletions(-) diff --git a/Lib/platform.py b/Lib/platform.py index a62192589af8ff..2a023cb43835a5 100644 --- a/Lib/platform.py +++ b/Lib/platform.py @@ -1464,9 +1464,24 @@ def invalidate_caches(): ### Command line interface -if __name__ == '__main__': - # Default is to print the aliased verbose platform string - terse = ('terse' in sys.argv or '--terse' in sys.argv) - aliased = (not 'nonaliased' in sys.argv and not '--nonaliased' in sys.argv) - print(platform(aliased, terse)) - sys.exit(0) +def _parse_args(args: list[str] | None): + import argparse + + parser = argparse.ArgumentParser() + parser.add_argument("--terse", action="store_true") + parser.add_argument("--nonaliased", action="store_true") + + return parser.parse_args(args) + + +def _main(args: list[str] | None = None): + args = _parse_args(args) + + aliased = not args.nonaliased + + print(platform(aliased, args.terse)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(_main()) diff --git a/Lib/test/test_platform.py b/Lib/test/test_platform.py index 6ba630ad527f91..3f53cb22259d2e 100644 --- a/Lib/test/test_platform.py +++ b/Lib/test/test_platform.py @@ -1,11 +1,15 @@ +import io +import itertools import os import copy +import contextlib import pickle import platform import subprocess import sys import unittest from unittest import mock +from textwrap import dedent from test import support from test.support import os_helper @@ -741,5 +745,52 @@ def test_parse_os_release(self): self.assertEqual(len(info["SPECIALS"]), 5) +class CommandLineTest(unittest.TestCase): + def setUp(self): + self.clear_caches() + self.addCleanup(self.clear_caches) + + def clear_caches(self): + platform._platform_cache.clear() + platform._sys_version_cache.clear() + platform._uname_cache = None + platform._os_release_cache = None + + @staticmethod + def text_normalize(string): + """Dedent *string* and strip it from its surrounding whitespaces. + This method is used by the other utility functions so that any + string to write or to match against can be freely indented. + """ + return dedent(string).strip() + + def invoke_platform(self, *flags): + output = io.StringIO() + with contextlib.redirect_stdout(output): + platform._main(args=flags) + return self.text_normalize(output.getvalue()) + + def test_unknown_flag(self): + with self.assertRaises(SystemExit): + # suppress argparse error message + with contextlib.redirect_stderr(io.StringIO()): + _ = self.invoke_platform('--unknown') + + def test_invocation(self): + self.invoke_platform("--terse", "--nonaliased") + self.invoke_platform("--nonaliased") + self.invoke_platform("--terse") + self.invoke_platform() + + def test_help(self): + output = io.StringIO() + + with self.assertRaises(SystemExit): + with contextlib.redirect_stdout(output): + platform._main(args=["--help"]) + + self.assertIn("usage:", output.getvalue()) + + if __name__ == '__main__': unittest.main() From 119de844f0a13154adbefa4d4a993ae93ab928d9 Mon Sep 17 00:00:00 2001 From: Harry Lees Date: Fri, 21 Mar 2025 16:41:28 +0000 Subject: [PATCH 02/16] Exit using sys.exit(0) --- Lib/platform.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/platform.py b/Lib/platform.py index 2a023cb43835a5..d609ca3ed78dba 100644 --- a/Lib/platform.py +++ b/Lib/platform.py @@ -1480,8 +1480,8 @@ def _main(args: list[str] | None = None): aliased = not args.nonaliased print(platform(aliased, args.terse)) - return 0 if __name__ == "__main__": - raise SystemExit(_main()) + _main() + sys.exit(0) From 994f46fe90aade9948ea1e7208f37b35a8804133 Mon Sep 17 00:00:00 2001 From: Harry Lees Date: Fri, 21 Mar 2025 17:19:38 +0000 Subject: [PATCH 03/16] Parse position and flag arguments --- Lib/platform.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Lib/platform.py b/Lib/platform.py index d609ca3ed78dba..8a74294926c460 100644 --- a/Lib/platform.py +++ b/Lib/platform.py @@ -1468,8 +1468,9 @@ def _parse_args(args: list[str] | None): import argparse parser = argparse.ArgumentParser() + parser.add_argument("args", nargs="*", choices=["nonaliased", "terse"]) parser.add_argument("--terse", action="store_true") - parser.add_argument("--nonaliased", action="store_true") + parser.add_argument("--nonaliased", dest="aliased", action="store_false") return parser.parse_args(args) @@ -1477,9 +1478,10 @@ def _parse_args(args: list[str] | None): def _main(args: list[str] | None = None): args = _parse_args(args) - aliased = not args.nonaliased + terse = args.terse or ("terse" in args.args) + aliased = args.aliased and ('nonaliased' not in args.args) - print(platform(aliased, args.terse)) + print(platform(aliased, terse)) if __name__ == "__main__": From a0bb407a12f1bd6332156ad8dbce8a757c9ab7d1 Mon Sep 17 00:00:00 2001 From: Harry Lees Date: Fri, 21 Mar 2025 17:19:49 +0000 Subject: [PATCH 04/16] test flag parsing --- Lib/test/test_platform.py | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_platform.py b/Lib/test/test_platform.py index 3f53cb22259d2e..1718edfe55dd36 100644 --- a/Lib/test/test_platform.py +++ b/Lib/test/test_platform.py @@ -6,6 +6,7 @@ import pickle import platform import subprocess +import shlex import sys import unittest from unittest import mock @@ -777,10 +778,36 @@ def test_unknown_flag(self): _ = self.invoke_platform('--unknown') def test_invocation(self): - self.invoke_platform("--terse", "--nonaliased") - self.invoke_platform("--nonaliased") - self.invoke_platform("--terse") - self.invoke_platform() + flags = ( + "--terse", "--nonaliased", "terse", "nonaliased" + ) + + for r in range(len(flags) + 1): + for combination in itertools.combinations(flags, r): + self.invoke_platform(*combination) + + def test_arg_parsing(self): + # Due to backwards compatibility, the `aliased` and `terse` parameters + # are computed based on a combination of positional arguments and flags. + # + # This test tests that the arguments are correctly passed to the underlying + # `platform.platform()` call. The parameters are two booleans for `aliased` + # and `terse` + options = ( + ("--nonaliased", (False, False)), + ("nonaliased", (False, False)), + ("--terse", (True, True)), + ("terse", (True, True)), + ("nonaliased terse", (False, True)), + ("--nonaliased terse", (False, True)), + ("--terse nonaliased", (False, True)), + ) + + for flags, args in options: + with self.subTest(f"{flags}, {args}"): + with mock.patch.object(platform, 'platform') as obj: + self.invoke_platform(*shlex.split(flags)) + obj.assert_called_once_with(*args) def test_help(self): output = io.StringIO() From a21a6bee2e3328e879695266e67bbc49bd864428 Mon Sep 17 00:00:00 2001 From: Harry Lees Date: Fri, 21 Mar 2025 17:34:38 +0000 Subject: [PATCH 05/16] Add NEWS entry --- .../next/Library/2025-03-21-17-34-27.gh-issue-131524.Vj1pO_.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2025-03-21-17-34-27.gh-issue-131524.Vj1pO_.rst diff --git a/Misc/NEWS.d/next/Library/2025-03-21-17-34-27.gh-issue-131524.Vj1pO_.rst b/Misc/NEWS.d/next/Library/2025-03-21-17-34-27.gh-issue-131524.Vj1pO_.rst new file mode 100644 index 00000000000000..28926d06ca4f93 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-03-21-17-34-27.gh-issue-131524.Vj1pO_.rst @@ -0,0 +1,2 @@ +Add help message to :mod:`platform` command-line interface. Contributed by +Harry Lees. From b4e0ed2e1e122590c2867686deff9f462a6ffe3e Mon Sep 17 00:00:00 2001 From: Harry Date: Fri, 21 Mar 2025 22:41:57 +0000 Subject: [PATCH 06/16] Sort imports alphabetically Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- Lib/test/test_platform.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_platform.py b/Lib/test/test_platform.py index 1718edfe55dd36..dcdc56a8e7739d 100644 --- a/Lib/test/test_platform.py +++ b/Lib/test/test_platform.py @@ -5,8 +5,8 @@ import contextlib import pickle import platform -import subprocess import shlex +import subprocess import sys import unittest from unittest import mock From 67cebfdc689dfc07bbee70e37cd64181d97c1ca7 Mon Sep 17 00:00:00 2001 From: Harry Date: Fri, 21 Mar 2025 22:42:08 +0000 Subject: [PATCH 07/16] Sort imports alphabetically Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- Lib/test/test_platform.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_platform.py b/Lib/test/test_platform.py index dcdc56a8e7739d..33501a1ba08349 100644 --- a/Lib/test/test_platform.py +++ b/Lib/test/test_platform.py @@ -1,8 +1,8 @@ -import io -import itertools import os import copy import contextlib +import io +import itertools import pickle import platform import shlex From 9ca5a9699a27422997d43d8074ec802f4016d2f5 Mon Sep 17 00:00:00 2001 From: Harry Lees Date: Fri, 21 Mar 2025 22:54:36 +0000 Subject: [PATCH 08/16] Add help messages to included commands --- Lib/platform.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Lib/platform.py b/Lib/platform.py index 8a74294926c460..bb6af7f174d7d7 100644 --- a/Lib/platform.py +++ b/Lib/platform.py @@ -1469,8 +1469,17 @@ def _parse_args(args: list[str] | None): parser = argparse.ArgumentParser() parser.add_argument("args", nargs="*", choices=["nonaliased", "terse"]) - parser.add_argument("--terse", action="store_true") - parser.add_argument("--nonaliased", dest="aliased", action="store_false") + parser.add_argument( + "--terse", + action="store_true", + help="return only the absolute minimum information needed to identify the platform", + ) + parser.add_argument( + "--nonaliased", + dest="aliased", + action="store_false", + help="prevent the system/ OS name from being aliased to common marketing names e.g. win32 instead of Windows" + ) return parser.parse_args(args) From 2c9a01c09f8958e6db689aabd3a43c9e8a83165f Mon Sep 17 00:00:00 2001 From: Harry Lees Date: Fri, 21 Mar 2025 23:08:24 +0000 Subject: [PATCH 09/16] Add help messages to included commands --- Lib/platform.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Lib/platform.py b/Lib/platform.py index bb6af7f174d7d7..32e81081a47a26 100644 --- a/Lib/platform.py +++ b/Lib/platform.py @@ -1472,13 +1472,16 @@ def _parse_args(args: list[str] | None): parser.add_argument( "--terse", action="store_true", - help="return only the absolute minimum information needed to identify the platform", + help=("return only the absolute minimum information needed to identify the " + "platform"), ) parser.add_argument( "--nonaliased", dest="aliased", action="store_false", - help="prevent the system/ OS name from being aliased to common marketing names e.g. win32 instead of Windows" + help=("disable system/ OS name aliasing. If aliasing is enabled, some " + "platforms will report system names which differ from their common " + "names, e.g. SunOS will be reported as Solaris"), ) return parser.parse_args(args) From fb133e1da43363e388ec498dd6d2dcd0fce8fb7e Mon Sep 17 00:00:00 2001 From: Harry Date: Sun, 23 Mar 2025 12:42:44 +0000 Subject: [PATCH 10/16] Use `platform.invalidate_caches()` in test setUp and teardown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Lib/test/test_platform.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/Lib/test/test_platform.py b/Lib/test/test_platform.py index 33501a1ba08349..eae60af2e4ce23 100644 --- a/Lib/test/test_platform.py +++ b/Lib/test/test_platform.py @@ -748,14 +748,8 @@ def test_parse_os_release(self): class CommandLineTest(unittest.TestCase): def setUp(self): - self.clear_caches() - self.addCleanup(self.clear_caches) - - def clear_caches(self): - platform._platform_cache.clear() - platform._sys_version_cache.clear() - platform._uname_cache = None - platform._os_release_cache = None + platform.invalidate_caches() + self.addCleanup(platform.invalidate_caches) @staticmethod def text_normalize(string): From 5fd460120d609f117a291a60ca43eced66383aae Mon Sep 17 00:00:00 2001 From: Harry Date: Sun, 23 Mar 2025 12:43:03 +0000 Subject: [PATCH 11/16] Fix typo in `Lib/test/test_platform.py` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Lib/test/test_platform.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_platform.py b/Lib/test/test_platform.py index eae60af2e4ce23..75f9a63bbf04cc 100644 --- a/Lib/test/test_platform.py +++ b/Lib/test/test_platform.py @@ -786,7 +786,7 @@ def test_arg_parsing(self): # # This test tests that the arguments are correctly passed to the underlying # `platform.platform()` call. The parameters are two booleans for `aliased` - # and `terse` + # and `terse`. options = ( ("--nonaliased", (False, False)), ("nonaliased", (False, False)), From 63814703c88514851785094ada202a2b702aa266 Mon Sep 17 00:00:00 2001 From: Harry Date: Sun, 23 Mar 2025 12:47:00 +0000 Subject: [PATCH 12/16] Update docstring in `text_normalize` function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Lib/test/test_platform.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/test/test_platform.py b/Lib/test/test_platform.py index 75f9a63bbf04cc..3cc0b1f60378df 100644 --- a/Lib/test/test_platform.py +++ b/Lib/test/test_platform.py @@ -754,6 +754,7 @@ def setUp(self): @staticmethod def text_normalize(string): """Dedent *string* and strip it from its surrounding whitespaces. + This method is used by the other utility functions so that any string to write or to match against can be freely indented. """ From 1f230f24a015e597f983158dc635eb3cde77253f Mon Sep 17 00:00:00 2001 From: Harry Lees Date: Mon, 24 Mar 2025 13:26:43 +0000 Subject: [PATCH 13/16] re-flow strings to improve formatting --- Lib/platform.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/platform.py b/Lib/platform.py index 32e81081a47a26..f07ece8f2bf4aa 100644 --- a/Lib/platform.py +++ b/Lib/platform.py @@ -1479,9 +1479,9 @@ def _parse_args(args: list[str] | None): "--nonaliased", dest="aliased", action="store_false", - help=("disable system/ OS name aliasing. If aliasing is enabled, some " - "platforms will report system names which differ from their common " - "names, e.g. SunOS will be reported as Solaris"), + help=("disable system/OS name aliasing. If aliasing is enabled, " + "some platforms will report system names which differ from " + "their common names, e.g. SunOS will be reported as Solaris"), ) return parser.parse_args(args) From a5dcbf25c228c6fdc9ff9252f325e5169d75133e Mon Sep 17 00:00:00 2001 From: Harry Lees Date: Mon, 24 Mar 2025 13:27:39 +0000 Subject: [PATCH 14/16] Convert shlex call to an array of flags --- Lib/test/test_platform.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/Lib/test/test_platform.py b/Lib/test/test_platform.py index 3cc0b1f60378df..5ec5a05b2991fe 100644 --- a/Lib/test/test_platform.py +++ b/Lib/test/test_platform.py @@ -5,12 +5,11 @@ import itertools import pickle import platform -import shlex import subprocess import sys import unittest -from unittest import mock from textwrap import dedent +from unittest import mock from test import support from test.support import os_helper @@ -789,19 +788,19 @@ def test_arg_parsing(self): # `platform.platform()` call. The parameters are two booleans for `aliased` # and `terse`. options = ( - ("--nonaliased", (False, False)), - ("nonaliased", (False, False)), - ("--terse", (True, True)), - ("terse", (True, True)), - ("nonaliased terse", (False, True)), - ("--nonaliased terse", (False, True)), - ("--terse nonaliased", (False, True)), + (["--nonaliased"], (False, False)), + (["nonaliased"], (False, False)), + (["--terse"], (True, True)), + (["terse"], (True, True)), + (["nonaliased", "terse"], (False, True)), + (["--nonaliased", "terse"], (False, True)), + (["--terse", "nonaliased"], (False, True)), ) for flags, args in options: - with self.subTest(f"{flags}, {args}"): + with self.subTest(flags=flags, args=args): with mock.patch.object(platform, 'platform') as obj: - self.invoke_platform(*shlex.split(flags)) + self.invoke_platform(*flags) obj.assert_called_once_with(*args) def test_help(self): From 48e355907090c43bdfffd76e1f4234bb58708230 Mon Sep 17 00:00:00 2001 From: Harry Lees Date: Thu, 24 Apr 2025 19:03:48 +0100 Subject: [PATCH 15/16] Remove redundant textwrap.dedent() --- Lib/test/test_platform.py | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/Lib/test/test_platform.py b/Lib/test/test_platform.py index 5ec5a05b2991fe..46d3db05f75896 100644 --- a/Lib/test/test_platform.py +++ b/Lib/test/test_platform.py @@ -1,14 +1,13 @@ -import os -import copy import contextlib +import copy import io import itertools +import os import pickle import platform import subprocess import sys import unittest -from textwrap import dedent from unittest import mock from test import support @@ -750,20 +749,11 @@ def setUp(self): platform.invalidate_caches() self.addCleanup(platform.invalidate_caches) - @staticmethod - def text_normalize(string): - """Dedent *string* and strip it from its surrounding whitespaces. - - This method is used by the other utility functions so that any - string to write or to match against can be freely indented. - """ - return dedent(string).strip() - def invoke_platform(self, *flags): output = io.StringIO() with contextlib.redirect_stdout(output): platform._main(args=flags) - return self.text_normalize(output.getvalue()) + return output.getvalue() def test_unknown_flag(self): with self.assertRaises(SystemExit): From cd389f5e5e0e4202d7235e575ffa762072f4f946 Mon Sep 17 00:00:00 2001 From: Harry Lees Date: Thu, 24 Apr 2025 19:07:38 +0100 Subject: [PATCH 16/16] Check for start of help command in tests which show help --- Lib/test/test_platform.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_platform.py b/Lib/test/test_platform.py index 46d3db05f75896..87d0bcf344455c 100644 --- a/Lib/test/test_platform.py +++ b/Lib/test/test_platform.py @@ -757,9 +757,11 @@ def invoke_platform(self, *flags): def test_unknown_flag(self): with self.assertRaises(SystemExit): + output = io.StringIO() # suppress argparse error message - with contextlib.redirect_stderr(io.StringIO()): + with contextlib.redirect_stderr(output): _ = self.invoke_platform('--unknown') + self.assertStartsWith(output, "usage: ") def test_invocation(self): flags = ( @@ -800,7 +802,7 @@ def test_help(self): with contextlib.redirect_stdout(output): platform._main(args=["--help"]) - self.assertIn("usage:", output.getvalue()) + self.assertStartsWith(output.getvalue(), "usage:") if __name__ == '__main__':