From 3fbfbf53c202c437d95dfce888c414e1154db5f8 Mon Sep 17 00:00:00 2001 From: Shahar Naveh <50263213+ShaharNaveh@users.noreply.github.com> Date: Mon, 11 May 2026 18:43:49 +0300 Subject: [PATCH] Update `platform.py` to 3.14.4 (#7824) --- Lib/platform.py | 117 ++++++++++++++++++++++------- Lib/test/test_platform.py | 151 +++++++++++++++++++++++++++++++++++--- 2 files changed, 229 insertions(+), 39 deletions(-) mode change 100755 => 100644 Lib/platform.py diff --git a/Lib/platform.py b/Lib/platform.py old mode 100755 new mode 100644 index 1a533688a..33d153f63 --- a/Lib/platform.py +++ b/Lib/platform.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - """ This module tries to retrieve as much platform-identifying data as possible. It makes this information available via function APIs. @@ -33,6 +31,7 @@ # # # +# 1.0.9 - added invalidate_caches() function to invalidate cached values # 1.0.8 - changed Windows support to read version from kernel32.dll # 1.0.7 - added DEV_NULL # 1.0.6 - added linux_distribution() @@ -111,7 +110,7 @@ __copyright__ = """ """ -__version__ = '1.0.8' +__version__ = '1.0.9' import collections import os @@ -174,6 +173,11 @@ def libc_ver(executable=None, lib='', version='', chunksize=16384): """ if not executable: + if sys.platform == "emscripten": + # Emscripten's os.confstr reports that it is glibc, so special case + # it. + ver = ".".join(str(x) for x in sys._emscripten_info.emscripten_version) + return ("emscripten", ver) try: ver = os.confstr('CS_GNU_LIBC_VERSION') # parse 'glibc 2.28' as ('glibc', '2.28') @@ -190,22 +194,26 @@ def libc_ver(executable=None, lib='', version='', chunksize=16384): # sys.executable is not set. return lib, version - libc_search = re.compile(b'(__libc_init)' - b'|' - b'(GLIBC_([0-9.]+))' - b'|' - br'(libc(_\w+)?\.so(?:\.(\d[0-9.]*))?)', re.ASCII) + libc_search = re.compile(br""" + (__libc_init) + | (GLIBC_([0-9.]+)) + | (libc(_\w+)?\.so(?:\.(\d[0-9.]*))?) + | (musl-([0-9.]+)) + | ((?:libc\.|ld-)musl(?:-\w+)?.so(?:\.(\d[0-9.]*))?) + """, + re.ASCII | re.VERBOSE) V = _comparable_version # We use os.path.realpath() # here to work around problems with Cygwin not being # able to open symlinks for reading executable = os.path.realpath(executable) + ver = None with open(executable, 'rb') as f: binary = f.read(chunksize) pos = 0 while pos < len(binary): - if b'libc' in binary or b'GLIBC' in binary: + if b'libc' in binary or b'GLIBC' in binary or b'musl' in binary: m = libc_search.search(binary, pos) else: m = None @@ -217,26 +225,35 @@ def libc_ver(executable=None, lib='', version='', chunksize=16384): continue if not m: break - libcinit, glibc, glibcversion, so, threads, soversion = [ - s.decode('latin1') if s is not None else s - for s in m.groups()] + decoded_groups = [s.decode('latin1') if s is not None else s + for s in m.groups()] + (libcinit, glibc, glibcversion, so, threads, soversion, + musl, muslversion, musl_so, musl_sover) = decoded_groups if libcinit and not lib: lib = 'libc' elif glibc: if lib != 'glibc': lib = 'glibc' - version = glibcversion - elif V(glibcversion) > V(version): - version = glibcversion + ver = glibcversion + elif V(glibcversion) > V(ver): + ver = glibcversion elif so: - if lib != 'glibc': + if lib not in ('glibc', 'musl'): lib = 'libc' - if soversion and (not version or V(soversion) > V(version)): - version = soversion - if threads and version[-len(threads):] != threads: - version = version + threads + if soversion and (not ver or V(soversion) > V(ver)): + ver = soversion + if threads and ver[-len(threads):] != threads: + ver = ver + threads + elif musl: + lib = 'musl' + if not ver or V(muslversion) > V(ver): + ver = muslversion + elif musl_so: + lib = 'musl' + if musl_sover and (not ver or V(musl_sover) > V(ver)): + ver = musl_sover pos = m.end() - return lib, version + return lib, version if ver is None else ver def _norm_version(version, build=''): @@ -549,7 +566,7 @@ def java_ver(release='', vendor='', vminfo=('', '', ''), osinfo=('', '', '')): warnings._deprecated('java_ver', remove=(3, 15)) # Import the needed APIs try: - import java.lang + import java.lang # noqa: F401 except ImportError: return release, vendor, vminfo, osinfo @@ -1192,7 +1209,7 @@ def _sys_version(sys_version=None): # CPython cpython_sys_version_parser = re.compile( r'([\w.+]+)\s*' # "version" - r'(?:experimental free-threading build\s+)?' # "free-threading-build" + r'(?:free-threading build\s+)?' # "free-threading-build" r'\(#?([^,]+)' # "(#buildno" r'(?:,\s*([\w ]*)' # ", builddate" r'(?:,\s*([\w :]*))?)?\)\s*' # ", buildtime)" @@ -1449,11 +1466,55 @@ def freedesktop_os_release(): return _os_release_cache.copy() +def invalidate_caches(): + """Invalidate the cached results.""" + global _uname_cache + _uname_cache = None + + global _os_release_cache + _os_release_cache = None + + _sys_version_cache.clear() + _platform_cache.clear() + + ### 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) +def _parse_args(args: list[str] | None): + import argparse + + parser = argparse.ArgumentParser(color=True) + parser.add_argument("args", nargs="*", choices=["nonaliased", "terse"]) + 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=( + "disable system/OS name aliasing. If aliasing is enabled, " + "some platforms report system names different from " + "their common names, e.g. SunOS is reported as Solaris" + ), + ) + + return parser.parse_args(args) + + +def _main(args: list[str] | None = None): + args = _parse_args(args) + + terse = args.terse or ("terse" in args.args) + aliased = args.aliased and ('nonaliased' not in args.args) + print(platform(aliased, terse)) - sys.exit(0) + + +if __name__ == "__main__": + _main() diff --git a/Lib/test/test_platform.py b/Lib/test/test_platform.py index ed277276b..e879e4857 100644 --- a/Lib/test/test_platform.py +++ b/Lib/test/test_platform.py @@ -1,5 +1,8 @@ -import os +import contextlib import copy +import io +import itertools +import os import pickle import platform import subprocess @@ -83,6 +86,38 @@ class PlatformTest(unittest.TestCase): platform._uname_cache = None platform._os_release_cache = None + def test_invalidate_caches(self): + self.clear_caches() + + self.assertDictEqual(platform._platform_cache, {}) + self.assertDictEqual(platform._sys_version_cache, {}) + self.assertIsNone(platform._uname_cache) + self.assertIsNone(platform._os_release_cache) + + # fill the cached entries (some have side effects on others) + platform.platform() # for platform._platform_cache + platform.python_implementation() # for platform._sys_version_cache + platform.uname() # for platform._uname_cache + + # check that the cache are filled + self.assertNotEqual(platform._platform_cache, {}) + self.assertNotEqual(platform._sys_version_cache, {}) + self.assertIsNotNone(platform._uname_cache) + + try: + platform.freedesktop_os_release() + except OSError: + self.assertIsNone(platform._os_release_cache) + else: + self.assertIsNotNone(platform._os_release_cache) + + with self.subTest('clear platform caches'): + platform.invalidate_caches() + self.assertDictEqual(platform._platform_cache, {}) + self.assertDictEqual(platform._sys_version_cache, {}) + self.assertIsNone(platform._uname_cache) + self.assertIsNone(platform._os_release_cache) + def test_architecture(self): res = platform.architecture() @@ -375,7 +410,7 @@ class PlatformTest(unittest.TestCase): for v in version.split('.'): int(v) # should not fail if csd: - self.assertTrue(csd.startswith('SP'), msg=csd) + self.assertStartsWith(csd, 'SP') if ptype: if os.cpu_count() > 1: self.assertIn('Multiprocessor', ptype) @@ -490,8 +525,10 @@ class PlatformTest(unittest.TestCase): self.assertEqual(override.model, "Whiz") self.assertTrue(override.is_simulator) - @unittest.skipIf(support.is_emscripten, "Does not apply to Emscripten") def test_libc_ver(self): + if support.is_emscripten: + assert platform.libc_ver() == ("emscripten", "4.0.12") + return # check that libc_ver(executable) doesn't raise an exception if os.path.isdir(sys.executable) and \ os.path.exists(sys.executable+'.exe'): @@ -519,6 +556,14 @@ class PlatformTest(unittest.TestCase): (b'GLIBC_2.9', ('glibc', '2.9')), (b'libc.so.1.2.5', ('libc', '1.2.5')), (b'libc_pthread.so.1.2.5', ('libc', '1.2.5_pthread')), + (b'/aports/main/musl/src/musl-1.2.5', ('musl', '1.2.5')), + # musl uses semver, but we accept some variations anyway: + (b'/aports/main/musl/src/musl-12.5', ('musl', '12.5')), + (b'/aports/main/musl/src/musl-1.2.5.7', ('musl', '1.2.5.7')), + (b'libc.musl.so.1', ('musl', '1')), + (b'libc.musl-x86_64.so.1.2.5', ('musl', '1.2.5')), + (b'ld-musl.so.1', ('musl', '1')), + (b'ld-musl-x86_64.so.1.2.5', ('musl', '1.2.5')), (b'', ('', '')), ): with open(filename, 'wb') as fp: @@ -530,14 +575,37 @@ class PlatformTest(unittest.TestCase): expected) # binary containing multiple versions: get the most recent, - # make sure that 1.9 is seen as older than 1.23.4 - chunksize = 16384 - with open(filename, 'wb') as f: - # test match at chunk boundary - f.write(b'x'*(chunksize - 10)) - f.write(b'GLIBC_1.23.4\0GLIBC_1.9\0GLIBC_1.21\0') - self.assertEqual(platform.libc_ver(filename, chunksize=chunksize), - ('glibc', '1.23.4')) + # make sure that eg 1.9 is seen as older than 1.23.4, and that + # the arguments don't count even if they are set. + chunksize = 200 + for data, expected in ( + (b'GLIBC_1.23.4\0GLIBC_1.9\0GLIBC_1.21\0', ('glibc', '1.23.4')), + (b'libc.so.2.4\0libc.so.9\0libc.so.23.1\0', ('libc', '23.1')), + (b'musl-1.4.1\0musl-2.1.1\0musl-2.0.1\0', ('musl', '2.1.1')), + ( + b'libc.musl-x86_64.so.1.4.1\0libc.musl-x86_64.so.2.1.1\0libc.musl-x86_64.so.2.0.1', + ('musl', '2.1.1'), + ), + ( + b'ld-musl-x86_64.so.1.4.1\0ld-musl-x86_64.so.2.1.1\0ld-musl-x86_64.so.2.0.1', + ('musl', '2.1.1'), + ), + (b'no match here, so defaults are used', ('test', '100.1.0')), + ): + with open(filename, 'wb') as f: + # test match at chunk boundary + f.write(b'x'*(chunksize - 10)) + f.write(data) + self.assertEqual( + expected, + platform.libc_ver( + filename, + lib='test', + version='100.1.0', + chunksize=chunksize, + ), + ) + def test_android_ver(self): res = platform.android_ver() @@ -690,5 +758,66 @@ class PlatformTest(unittest.TestCase): self.assertEqual(len(info["SPECIALS"]), 5) +class CommandLineTest(unittest.TestCase): + def setUp(self): + platform.invalidate_caches() + self.addCleanup(platform.invalidate_caches) + + def invoke_platform(self, *flags): + output = io.StringIO() + with contextlib.redirect_stdout(output): + platform._main(args=flags) + return output.getvalue() + + def test_unknown_flag(self): + with self.assertRaises(SystemExit): + output = io.StringIO() + # suppress argparse error message + with contextlib.redirect_stderr(output): + _ = self.invoke_platform('--unknown') + self.assertStartsWith(output, "usage: ") + + def test_invocation(self): + 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): + # For backwards compatibility, the `aliased` and `terse` parameters are + # computed based on a combination of positional arguments and flags. + # + # Test that the arguments are correctly passed to the underlying + # `platform.platform()` call. + 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, aliased, terse in options: + with self.subTest(flags=flags, aliased=aliased, terse=terse): + with mock.patch.object(platform, 'platform') as obj: + self.invoke_platform(*flags) + obj.assert_called_once_with(aliased, terse) + + @support.force_not_colorized + def test_help(self): + output = io.StringIO() + + with self.assertRaises(SystemExit): + with contextlib.redirect_stdout(output): + platform._main(args=["--help"]) + + self.assertStartsWith(output.getvalue(), "usage:") + + if __name__ == '__main__': unittest.main()