Update platform.py to 3.14.4 (#7824)

This commit is contained in:
Shahar Naveh
2026-05-11 18:43:49 +03:00
committed by GitHub
parent 3bd79e6b5b
commit 3fbfbf53c2
2 changed files with 229 additions and 39 deletions

117
Lib/platform.py vendored Executable file → Normal file
View File

@@ -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 @@
#
# <see CVS and SVN checkin messages for history>
#
# 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<space>"
r'(?:experimental free-threading build\s+)?' # "free-threading-build<space>"
r'(?:free-threading build\s+)?' # "free-threading-build<space>"
r'\(#?([^,]+)' # "(#buildno"
r'(?:,\s*([\w ]*)' # ", builddate"
r'(?:,\s*([\w :]*))?)?\)\s*' # ", buildtime)<space>"
@@ -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()

View File

@@ -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()