Skip to content

Commit 3fbfbf5

Browse files
authored
Update platform.py to 3.14.4 (#7824)
1 parent 3bd79e6 commit 3fbfbf5

2 files changed

Lines changed: 229 additions & 39 deletions

File tree

Lib/platform.py

100755100644
Lines changed: 89 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
#!/usr/bin/env python3
2-
31
""" This module tries to retrieve as much platform-identifying data as
42
possible. It makes this information available via function APIs.
53
@@ -33,6 +31,7 @@
3331
#
3432
# <see CVS and SVN checkin messages for history>
3533
#
34+
# 1.0.9 - added invalidate_caches() function to invalidate cached values
3635
# 1.0.8 - changed Windows support to read version from kernel32.dll
3736
# 1.0.7 - added DEV_NULL
3837
# 1.0.6 - added linux_distribution()
@@ -111,7 +110,7 @@
111110
112111
"""
113112

114-
__version__ = '1.0.8'
113+
__version__ = '1.0.9'
115114

116115
import collections
117116
import os
@@ -174,6 +173,11 @@ def libc_ver(executable=None, lib='', version='', chunksize=16384):
174173
175174
"""
176175
if not executable:
176+
if sys.platform == "emscripten":
177+
# Emscripten's os.confstr reports that it is glibc, so special case
178+
# it.
179+
ver = ".".join(str(x) for x in sys._emscripten_info.emscripten_version)
180+
return ("emscripten", ver)
177181
try:
178182
ver = os.confstr('CS_GNU_LIBC_VERSION')
179183
# parse 'glibc 2.28' as ('glibc', '2.28')
@@ -190,22 +194,26 @@ def libc_ver(executable=None, lib='', version='', chunksize=16384):
190194
# sys.executable is not set.
191195
return lib, version
192196

193-
libc_search = re.compile(b'(__libc_init)'
194-
b'|'
195-
b'(GLIBC_([0-9.]+))'
196-
b'|'
197-
br'(libc(_\w+)?\.so(?:\.(\d[0-9.]*))?)', re.ASCII)
197+
libc_search = re.compile(br"""
198+
(__libc_init)
199+
| (GLIBC_([0-9.]+))
200+
| (libc(_\w+)?\.so(?:\.(\d[0-9.]*))?)
201+
| (musl-([0-9.]+))
202+
| ((?:libc\.|ld-)musl(?:-\w+)?.so(?:\.(\d[0-9.]*))?)
203+
""",
204+
re.ASCII | re.VERBOSE)
198205

199206
V = _comparable_version
200207
# We use os.path.realpath()
201208
# here to work around problems with Cygwin not being
202209
# able to open symlinks for reading
203210
executable = os.path.realpath(executable)
211+
ver = None
204212
with open(executable, 'rb') as f:
205213
binary = f.read(chunksize)
206214
pos = 0
207215
while pos < len(binary):
208-
if b'libc' in binary or b'GLIBC' in binary:
216+
if b'libc' in binary or b'GLIBC' in binary or b'musl' in binary:
209217
m = libc_search.search(binary, pos)
210218
else:
211219
m = None
@@ -217,26 +225,35 @@ def libc_ver(executable=None, lib='', version='', chunksize=16384):
217225
continue
218226
if not m:
219227
break
220-
libcinit, glibc, glibcversion, so, threads, soversion = [
221-
s.decode('latin1') if s is not None else s
222-
for s in m.groups()]
228+
decoded_groups = [s.decode('latin1') if s is not None else s
229+
for s in m.groups()]
230+
(libcinit, glibc, glibcversion, so, threads, soversion,
231+
musl, muslversion, musl_so, musl_sover) = decoded_groups
223232
if libcinit and not lib:
224233
lib = 'libc'
225234
elif glibc:
226235
if lib != 'glibc':
227236
lib = 'glibc'
228-
version = glibcversion
229-
elif V(glibcversion) > V(version):
230-
version = glibcversion
237+
ver = glibcversion
238+
elif V(glibcversion) > V(ver):
239+
ver = glibcversion
231240
elif so:
232-
if lib != 'glibc':
241+
if lib not in ('glibc', 'musl'):
233242
lib = 'libc'
234-
if soversion and (not version or V(soversion) > V(version)):
235-
version = soversion
236-
if threads and version[-len(threads):] != threads:
237-
version = version + threads
243+
if soversion and (not ver or V(soversion) > V(ver)):
244+
ver = soversion
245+
if threads and ver[-len(threads):] != threads:
246+
ver = ver + threads
247+
elif musl:
248+
lib = 'musl'
249+
if not ver or V(muslversion) > V(ver):
250+
ver = muslversion
251+
elif musl_so:
252+
lib = 'musl'
253+
if musl_sover and (not ver or V(musl_sover) > V(ver)):
254+
ver = musl_sover
238255
pos = m.end()
239-
return lib, version
256+
return lib, version if ver is None else ver
240257

241258
def _norm_version(version, build=''):
242259

@@ -549,7 +566,7 @@ def java_ver(release='', vendor='', vminfo=('', '', ''), osinfo=('', '', '')):
549566
warnings._deprecated('java_ver', remove=(3, 15))
550567
# Import the needed APIs
551568
try:
552-
import java.lang
569+
import java.lang # noqa: F401
553570
except ImportError:
554571
return release, vendor, vminfo, osinfo
555572

@@ -1192,7 +1209,7 @@ def _sys_version(sys_version=None):
11921209
# CPython
11931210
cpython_sys_version_parser = re.compile(
11941211
r'([\w.+]+)\s*' # "version<space>"
1195-
r'(?:experimental free-threading build\s+)?' # "free-threading-build<space>"
1212+
r'(?:free-threading build\s+)?' # "free-threading-build<space>"
11961213
r'\(#?([^,]+)' # "(#buildno"
11971214
r'(?:,\s*([\w ]*)' # ", builddate"
11981215
r'(?:,\s*([\w :]*))?)?\)\s*' # ", buildtime)<space>"
@@ -1449,11 +1466,55 @@ def freedesktop_os_release():
14491466
return _os_release_cache.copy()
14501467

14511468

1469+
def invalidate_caches():
1470+
"""Invalidate the cached results."""
1471+
global _uname_cache
1472+
_uname_cache = None
1473+
1474+
global _os_release_cache
1475+
_os_release_cache = None
1476+
1477+
_sys_version_cache.clear()
1478+
_platform_cache.clear()
1479+
1480+
14521481
### Command line interface
14531482

1454-
if __name__ == '__main__':
1455-
# Default is to print the aliased verbose platform string
1456-
terse = ('terse' in sys.argv or '--terse' in sys.argv)
1457-
aliased = (not 'nonaliased' in sys.argv and not '--nonaliased' in sys.argv)
1483+
def _parse_args(args: list[str] | None):
1484+
import argparse
1485+
1486+
parser = argparse.ArgumentParser(color=True)
1487+
parser.add_argument("args", nargs="*", choices=["nonaliased", "terse"])
1488+
parser.add_argument(
1489+
"--terse",
1490+
action="store_true",
1491+
help=(
1492+
"return only the absolute minimum information needed "
1493+
"to identify the platform"
1494+
),
1495+
)
1496+
parser.add_argument(
1497+
"--nonaliased",
1498+
dest="aliased",
1499+
action="store_false",
1500+
help=(
1501+
"disable system/OS name aliasing. If aliasing is enabled, "
1502+
"some platforms report system names different from "
1503+
"their common names, e.g. SunOS is reported as Solaris"
1504+
),
1505+
)
1506+
1507+
return parser.parse_args(args)
1508+
1509+
1510+
def _main(args: list[str] | None = None):
1511+
args = _parse_args(args)
1512+
1513+
terse = args.terse or ("terse" in args.args)
1514+
aliased = args.aliased and ('nonaliased' not in args.args)
1515+
14581516
print(platform(aliased, terse))
1459-
sys.exit(0)
1517+
1518+
1519+
if __name__ == "__main__":
1520+
_main()

Lib/test/test_platform.py

Lines changed: 140 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
import os
1+
import contextlib
22
import copy
3+
import io
4+
import itertools
5+
import os
36
import pickle
47
import platform
58
import subprocess
@@ -83,6 +86,38 @@ def clear_caches(self):
8386
platform._uname_cache = None
8487
platform._os_release_cache = None
8588

89+
def test_invalidate_caches(self):
90+
self.clear_caches()
91+
92+
self.assertDictEqual(platform._platform_cache, {})
93+
self.assertDictEqual(platform._sys_version_cache, {})
94+
self.assertIsNone(platform._uname_cache)
95+
self.assertIsNone(platform._os_release_cache)
96+
97+
# fill the cached entries (some have side effects on others)
98+
platform.platform() # for platform._platform_cache
99+
platform.python_implementation() # for platform._sys_version_cache
100+
platform.uname() # for platform._uname_cache
101+
102+
# check that the cache are filled
103+
self.assertNotEqual(platform._platform_cache, {})
104+
self.assertNotEqual(platform._sys_version_cache, {})
105+
self.assertIsNotNone(platform._uname_cache)
106+
107+
try:
108+
platform.freedesktop_os_release()
109+
except OSError:
110+
self.assertIsNone(platform._os_release_cache)
111+
else:
112+
self.assertIsNotNone(platform._os_release_cache)
113+
114+
with self.subTest('clear platform caches'):
115+
platform.invalidate_caches()
116+
self.assertDictEqual(platform._platform_cache, {})
117+
self.assertDictEqual(platform._sys_version_cache, {})
118+
self.assertIsNone(platform._uname_cache)
119+
self.assertIsNone(platform._os_release_cache)
120+
86121
def test_architecture(self):
87122
res = platform.architecture()
88123

@@ -375,7 +410,7 @@ def test_win32_ver(self):
375410
for v in version.split('.'):
376411
int(v) # should not fail
377412
if csd:
378-
self.assertTrue(csd.startswith('SP'), msg=csd)
413+
self.assertStartsWith(csd, 'SP')
379414
if ptype:
380415
if os.cpu_count() > 1:
381416
self.assertIn('Multiprocessor', ptype)
@@ -490,8 +525,10 @@ def test_ios_ver(self):
490525
self.assertEqual(override.model, "Whiz")
491526
self.assertTrue(override.is_simulator)
492527

493-
@unittest.skipIf(support.is_emscripten, "Does not apply to Emscripten")
494528
def test_libc_ver(self):
529+
if support.is_emscripten:
530+
assert platform.libc_ver() == ("emscripten", "4.0.12")
531+
return
495532
# check that libc_ver(executable) doesn't raise an exception
496533
if os.path.isdir(sys.executable) and \
497534
os.path.exists(sys.executable+'.exe'):
@@ -519,6 +556,14 @@ def test_libc_ver(self):
519556
(b'GLIBC_2.9', ('glibc', '2.9')),
520557
(b'libc.so.1.2.5', ('libc', '1.2.5')),
521558
(b'libc_pthread.so.1.2.5', ('libc', '1.2.5_pthread')),
559+
(b'/aports/main/musl/src/musl-1.2.5', ('musl', '1.2.5')),
560+
# musl uses semver, but we accept some variations anyway:
561+
(b'/aports/main/musl/src/musl-12.5', ('musl', '12.5')),
562+
(b'/aports/main/musl/src/musl-1.2.5.7', ('musl', '1.2.5.7')),
563+
(b'libc.musl.so.1', ('musl', '1')),
564+
(b'libc.musl-x86_64.so.1.2.5', ('musl', '1.2.5')),
565+
(b'ld-musl.so.1', ('musl', '1')),
566+
(b'ld-musl-x86_64.so.1.2.5', ('musl', '1.2.5')),
522567
(b'', ('', '')),
523568
):
524569
with open(filename, 'wb') as fp:
@@ -530,14 +575,37 @@ def test_libc_ver(self):
530575
expected)
531576

532577
# binary containing multiple versions: get the most recent,
533-
# make sure that 1.9 is seen as older than 1.23.4
534-
chunksize = 16384
535-
with open(filename, 'wb') as f:
536-
# test match at chunk boundary
537-
f.write(b'x'*(chunksize - 10))
538-
f.write(b'GLIBC_1.23.4\0GLIBC_1.9\0GLIBC_1.21\0')
539-
self.assertEqual(platform.libc_ver(filename, chunksize=chunksize),
540-
('glibc', '1.23.4'))
578+
# make sure that eg 1.9 is seen as older than 1.23.4, and that
579+
# the arguments don't count even if they are set.
580+
chunksize = 200
581+
for data, expected in (
582+
(b'GLIBC_1.23.4\0GLIBC_1.9\0GLIBC_1.21\0', ('glibc', '1.23.4')),
583+
(b'libc.so.2.4\0libc.so.9\0libc.so.23.1\0', ('libc', '23.1')),
584+
(b'musl-1.4.1\0musl-2.1.1\0musl-2.0.1\0', ('musl', '2.1.1')),
585+
(
586+
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',
587+
('musl', '2.1.1'),
588+
),
589+
(
590+
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',
591+
('musl', '2.1.1'),
592+
),
593+
(b'no match here, so defaults are used', ('test', '100.1.0')),
594+
):
595+
with open(filename, 'wb') as f:
596+
# test match at chunk boundary
597+
f.write(b'x'*(chunksize - 10))
598+
f.write(data)
599+
self.assertEqual(
600+
expected,
601+
platform.libc_ver(
602+
filename,
603+
lib='test',
604+
version='100.1.0',
605+
chunksize=chunksize,
606+
),
607+
)
608+
541609

542610
def test_android_ver(self):
543611
res = platform.android_ver()
@@ -690,5 +758,66 @@ def test_parse_os_release(self):
690758
self.assertEqual(len(info["SPECIALS"]), 5)
691759

692760

761+
class CommandLineTest(unittest.TestCase):
762+
def setUp(self):
763+
platform.invalidate_caches()
764+
self.addCleanup(platform.invalidate_caches)
765+
766+
def invoke_platform(self, *flags):
767+
output = io.StringIO()
768+
with contextlib.redirect_stdout(output):
769+
platform._main(args=flags)
770+
return output.getvalue()
771+
772+
def test_unknown_flag(self):
773+
with self.assertRaises(SystemExit):
774+
output = io.StringIO()
775+
# suppress argparse error message
776+
with contextlib.redirect_stderr(output):
777+
_ = self.invoke_platform('--unknown')
778+
self.assertStartsWith(output, "usage: ")
779+
780+
def test_invocation(self):
781+
flags = (
782+
"--terse", "--nonaliased", "terse", "nonaliased"
783+
)
784+
785+
for r in range(len(flags) + 1):
786+
for combination in itertools.combinations(flags, r):
787+
self.invoke_platform(*combination)
788+
789+
def test_arg_parsing(self):
790+
# For backwards compatibility, the `aliased` and `terse` parameters are
791+
# computed based on a combination of positional arguments and flags.
792+
#
793+
# Test that the arguments are correctly passed to the underlying
794+
# `platform.platform()` call.
795+
options = (
796+
(["--nonaliased"], False, False),
797+
(["nonaliased"], False, False),
798+
(["--terse"], True, True),
799+
(["terse"], True, True),
800+
(["nonaliased", "terse"], False, True),
801+
(["--nonaliased", "terse"], False, True),
802+
(["--terse", "nonaliased"], False, True),
803+
)
804+
805+
for flags, aliased, terse in options:
806+
with self.subTest(flags=flags, aliased=aliased, terse=terse):
807+
with mock.patch.object(platform, 'platform') as obj:
808+
self.invoke_platform(*flags)
809+
obj.assert_called_once_with(aliased, terse)
810+
811+
@support.force_not_colorized
812+
def test_help(self):
813+
output = io.StringIO()
814+
815+
with self.assertRaises(SystemExit):
816+
with contextlib.redirect_stdout(output):
817+
platform._main(args=["--help"])
818+
819+
self.assertStartsWith(output.getvalue(), "usage:")
820+
821+
693822
if __name__ == '__main__':
694823
unittest.main()

0 commit comments

Comments
 (0)