8000 bpo-28468: Add platform.freedesktop_os_release() (GH-23492) · python/cpython@5c73afc · GitHub
[go: up one dir, main page]

Skip to content

Commit 5c73afc

Browse files
tiranvstinner
andauthored
bpo-28468: Add platform.freedesktop_os_release() (GH-23492)
Add platform.freedesktop_os_release() function to parse freedesktop.org os-release files. Signed-off-by: Christian Heimes <christian@python.org> Co-authored-by: Victor Stinner <vstinner@python.org>
1 parent 9bdc40e commit 5c73afc

File tree

5 files changed

+211
-0
lines changed

5 files changed

+211
-0
lines changed

Doc/library/platform.rst

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,3 +253,41 @@ Unix Platforms
253253
using :program:`gcc`.
254254

255255
The file is read and scanned in chunks of *chunksize* bytes.
256+
257+
258+
Linux Platforms
259+
---------------
260+
261+
.. function:: freedesktop_os_release()
262+
263+
Get operating system identification from ``os-release`` file and return
264+
it as a dict. The ``os-release`` file is a `freedesktop.org standard
265+
<https://www.freedesktop.org/software/systemd/man/os-release.html>`_ and
266+
is available in most Linux distributions. A noticeable exception is
267+
Android and Android-based distributions.
268+
269+
Raises :exc:`OSError` or subclass when neither ``/etc/os-release`` nor
270+
``/usr/lib/os-release`` can be read.
271+
272+
On success, the function returns a dictionary where keys and values are
273+
strings. Values have their special characters like ``"`` and ``$``
274+
unquoted. The fields ``NAME``, ``ID``, and ``PRETTY_NAME`` are always
275+
defined according to the standard. All other fields are optional. Vendors
276+
may include additional fields.
277+
278+
Note that fields like ``NAME``, ``VERSION``, and ``VARIANT`` are strings
279+
suitable for presentation to users. Programs should use fields like
280+
``ID``, ``ID_LIKE``, ``VERSION_ID``, or ``VARIANT_ID`` to identify
281+
Linux distributions.
282+
283+
Example::
284+
285+
def get_like_distro():
286+
info = platform.freedesktop_os_release()
287+
ids = [info["ID"]]
288+
if "ID_LIKE" in info:
289+
# ids are space separated and ordered by precedence
290+
ids.extend(info["ID_LIKE"].split())
291+
return ids
292+
293+
.. versionadded:: 3.10

Doc/whatsnew/3.10.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,14 @@ Added negative indexing support to :attr:`PurePath.parents
254254
<pathlib.PurePath.parents>`.
255255
(Contributed by Yaroslav Pankovych in :issue:`21041`)
256256

257+
platform
258+
--------
259+
260+
Added :func:`platform.freedesktop_os_release()` to retrieve operation system
261+
identification from `freedesktop.org os-release
262+
<https://www.freedesktop.org/software/systemd/man/os-release.html>`_ standard file.
263+
(Contributed by Christian Heimes in :issue:`28468`)
264+
257265
py_compile
258266
----------
259267

Lib/platform.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1230,6 +1230,63 @@ def platform(aliased=0, terse=0):
12301230
_platform_cache[(aliased, terse)] = platform
12311231
return platform
12321232

1233+
### freedesktop.org os-release standard
1234+
# https://www.freedesktop.org/software/systemd/man/os-release.html
1235+
1236+
# NAME=value with optional quotes (' or "). The regular expression is less
1237+
# strict than shell lexer, but that's ok.
1238+
_os_release_line = re.compile(
1239+
"^(?P<name>[a-zA-Z0-9_]+)=(?P<quote>[\"\']?)(?P<value>.*)(?P=quote)$"
1240+
)
1241+
# unescape five special characters mentioned in the standard
1242+
_os_release_unescape = re.compile(r"\\([\\\$\"\'`])")
1243+
# /etc takes precedence over /usr/lib
1244+
_os_release_candidates = ("/etc/os-release", "/usr/lib/os-relesase")
1245+
_os_release_cache = None
1246+
1247+
1248+
def _parse_os_release(lines):
1249+
# These fields are mandatory fields with well-known defaults
1250+
# in pratice all Linux distributions override NAME, ID, and PRETTY_NAME.
1251+
info = {
1252+
"NAME": "Linux",
1253+
"ID": "linux",
1254+
"PRETTY_NAME": "Linux",
1255+
}
1256+
1257+
for line in lines:
1258+
mo = _os_release_line.match(line)
1259+
if mo is not None:
1260+
info[mo.group('name')] = _os_release_unescape.sub(
1261+
r"\1", mo.group('value')
1262+
)
1263+
1264+
return info
1265+
1266+
1267+
def freedesktop_os_release():
1268+
"""Return operation system identification from freedesktop.org os-release
1269+
"""
1270+
global _os_release_cache
1271+
1272+
if _os_release_cache is None:
1273+
errno = None
1274+
for candidate in _os_release_candidates:
1275+
try:
1276+
with open(candidate, encoding="utf-8") as f:
1277+
_os_release_cache = _parse_os_release(f)
1278+
break
1279+
except OSError as e:
1280+
errno = e.errno
1281+
else:
1282+
raise OSError(
1283+
errno,
1284+
f"Unable to read files {', '.join(_os_release_candidates)}"
1285+
)
1286+
1287+
return _os_release_cache.copy()
1288+
1289+
12331290
### Command line interface
12341291

12351292
if __name__ == '__main__':

Lib/test/test_platform.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,70 @@
88
from test import support
99
from test.support import os_helper
1010

11+
FEDORA_OS_RELEASE = """\
12+
NAME=Fedora
13+
VERSION="32 (Thirty Two)"
14+
ID=fedora
15+
VERSION_ID=32
16+
VERSION_CODENAME=""
17+
PLATFORM_ID="platform:f32"
18+
PRETTY_NAME="Fedora 32 (Thirty Two)"
19+
ANSI_COLOR="0;34"
20+
LOGO=fedora-logo-icon
21+
CPE_NAME="cpe:/o:fedoraproject:fedora:32"
22+
HOME_URL="https://fedoraproject.org/"
23+
DOCUMENTATION_URL="https://docs.fedoraproject.org/en-US/fedora/f32/system-administrators-guide/"
24+
SUPPORT_URL="https://fedoraproject.org/wiki/Communicating_and_getting_help"
25+
BUG_REPORT_URL="https://bugzilla.redhat.com/"
26+
REDHAT_BUGZILLA_PRODUCT="Fedora"
27+
REDHAT_BUGZILLA_PRODUCT_VERSION=32
28+
REDHAT_SUPPORT_PRODUCT="Fedora"
29+
REDHAT_SUPPORT_PRODUCT_VERSION=32
30+
PRIVACY_POLICY_URL="https://fedoraproject.org/wiki/Legal:PrivacyPolicy"
31+
"""
32+
33+
UBUNTU_OS_RELEASE = """\
34+
NAME="Ubuntu"
35+
VERSION="20.04.1 LTS (Focal Fossa)"
36+
ID=ubuntu
37+
ID_LIKE=debian
38+
PRETTY_NAME="Ubuntu 20.04.1 LTS"
39+
VERSION_ID="20.04"
40+
HOME_URL="https://www.ubuntu.com/"
41+
SUPPORT_URL="https://help.ubuntu.com/"
42+
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
43+
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
44+
VERSION_CODENAME=focal
45+
UBUNTU_CODENAME=focal
46+
"""
47+
48+
TEST_OS_RELEASE = r"""
49+
# test data
50+
ID_LIKE="egg spam viking"
51+
EMPTY=
52+
# comments and empty lines are ignored
53+
54+
SINGLE_QUOTE='single'
55+
EMPTY_SINGLE=''
56+
DOUBLE_QUOTE="double"
57+
EMPTY_DOUBLE 10000 =""
58+
QUOTES="double\'s"
59+
SPECIALS="\$\`\\\'\""
60+
# invalid lines
61+
=invalid
62+
=
63+
INVALID
64+
IN-VALID=value
65+
IN VALID=value
66+
"""
67+
1168

1269
class PlatformTest(unittest.TestCase):
1370
def clear_caches(self):
1471
platform._platform_cache.clear()
1572
platform._sys_version_cache.clear()
1673
platform._uname_cache = None
74+
platform._os_release_cache = None
1775

1876
def test_architecture(self):
1977
res = platform.architecture()
@@ -382,6 +440,54 @@ def test_macos(self):
382440
self.assertEqual(platform.platform(terse=1), expected_terse)
383441
self.assertEqual(platform.platform(), expected)
384442

443+
def test_freedesktop_os_release(self):
444+
self.addCleanup(self.clear_caches)
445+
self.clear_caches()
446+
447+
if any(os.path.isfile(fn) for fn in platform._os_release_candidates):
448+
info = platform.freedesktop_os_release()
449+
self.assertIn("NAME", info)
450+
self.assertIn("ID", info)
451+
452+
info["CPYTHON_TEST"] = "test"
453+
self.assertNotIn(
454+
"CPYTHON_TEST",
455+
platform.freedesktop_os_release()
456+
)
457+
else:
458+
with self.assertRaises(OSError):
459+
platform.freedesktop_os_release()
460+
461+
def test_parse_os_release(self):
462+
info = platform._parse_os_release(FEDORA_OS_RELEASE.splitlines())
463+
self.assertEqual(info["NAME"], "Fedora")
464+
self.assertEqual(info["ID"], "fedora")
465+
self.assertNotIn("ID_LIKE", info)
466+
self.assertEqual(info["VERSION_CODENAME"], "")
467+
468+
info = platform._parse_os_release(UBUNTU_OS_RELEASE.splitlines())
469+
self.assertEqual(info["NAME"], "Ubuntu")
470+
self.assertEqual(info["ID"], "ubuntu")
471+
self.assertEqual(info["ID_LIKE"], "debian")
472+
self.assertEqual(info["VERSION_CODENAME"], "focal")
473+
474+
info = platform._parse_os_release(TEST_OS_RELEASE.splitlines())
475+
expected = {
476+
"ID": "linux",
477+
"NAME": "Linux",
478+
"PRETTY_NAME": "Linux",
479+
"ID_LIKE": "egg spam viking",
480+
"EMPTY": "",
481+
"DOUBLE_QUOTE": "double",
482+
"EMPTY_DOUBLE": "",
483+
"SINGLE_QUOTE": "single",
484+
"EMPTY_SINGLE": "",
485+
"QUOTES": "double's",
486+
"SPECIALS": "$`\\'\"",
487+
}
488+
self.assertEqual(info, expected)
489+
self.assertEqual(len(info["SPECIALS"]), 5)
490+
385491

386492
if __name__ == '__main__':
387493
unittest.main()
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add :func:`platform.freedesktop_os_release` function to parse freedesktop.org
2+
``os-release`` files.

0 commit comments

Comments
 (0)
0