summaryrefslogtreecommitdiffstats
path: root/Lib
diff options
context:
space:
mode:
authorChristian Heimes <christian@python.org>2020-11-30 21:34:45 (GMT)
committerGitHub <noreply@github.com>2020-11-30 21:34:45 (GMT)
commit5c73afc36ee6cca41009a510092e1f901c5dc0a0 (patch)
treeeb72eecfbbaf3809eead8a972f47064272b34fe3 /Lib
parent9bdc40ee3e0d886fb62b5334e8a88c1fe9460ba0 (diff)
downloadcpython-5c73afc36ee6cca41009a510092e1f901c5dc0a0.zip
cpython-5c73afc36ee6cca41009a510092e1f901c5dc0a0.tar.gz
cpython-5c73afc36ee6cca41009a510092e1f901c5dc0a0.tar.bz2
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>
Diffstat (limited to 'Lib')
-rwxr-xr-xLib/platform.py57
-rw-r--r--Lib/test/test_platform.py106
2 files changed, 163 insertions, 0 deletions
diff --git a/Lib/platform.py b/Lib/platform.py
index 0eb5167..138a974 100755
--- a/Lib/platform.py
+++ b/Lib/platform.py
@@ -1230,6 +1230,63 @@ def platform(aliased=0, terse=0):
_platform_cache[(aliased, terse)] = platform
return platform
+### freedesktop.org os-release standard
+# https://www.freedesktop.org/software/systemd/man/os-release.html
+
+# NAME=value with optional quotes (' or "). The regular expression is less
+# strict than shell lexer, but that's ok.
+_os_release_line = re.compile(
+ "^(?P<name>[a-zA-Z0-9_]+)=(?P<quote>[\"\']?)(?P<value>.*)(?P=quote)$"
+)
+# unescape five special characters mentioned in the standard
+_os_release_unescape = re.compile(r"\\([\\\$\"\'`])")
+# /etc takes precedence over /usr/lib
+_os_release_candidates = ("/etc/os-release", "/usr/lib/os-relesase")
+_os_release_cache = None
+
+
+def _parse_os_release(lines):
+ # These fields are mandatory fields with well-known defaults
+ # in pratice all Linux distributions override NAME, ID, and PRETTY_NAME.
+ info = {
+ "NAME": "Linux",
+ "ID": "linux",
+ "PRETTY_NAME": "Linux",
+ }
+
+ for line in lines:
+ mo = _os_release_line.match(line)
+ if mo is not None:
+ info[mo.group('name')] = _os_release_unescape.sub(
+ r"\1", mo.group('value')
+ )
+
+ return info
+
+
+def freedesktop_os_release():
+ """Return operation system identification from freedesktop.org os-release
+ """
+ global _os_release_cache
+
+ if _os_release_cache is None:
+ errno = None
+ for candidate in _os_release_candidates:
+ try:
+ with open(candidate, encoding="utf-8") as f:
+ _os_release_cache = _parse_os_release(f)
+ break
+ except OSError as e:
+ errno = e.errno
+ else:
+ raise OSError(
+ errno,
+ f"Unable to read files {', '.join(_os_release_candidates)}"
+ )
+
+ return _os_release_cache.copy()
+
+
### Command line interface
if __name__ == '__main__':
diff --git a/Lib/test/test_platform.py b/Lib/test/test_platform.py
index 1590cd5..2c6fbee 100644
--- a/Lib/test/test_platform.py
+++ b/Lib/test/test_platform.py
@@ -8,12 +8,70 @@ from unittest import mock
from test import support
from test.support import os_helper
+FEDORA_OS_RELEASE = """\
+NAME=Fedora
+VERSION="32 (Thirty Two)"
+ID=fedora
+VERSION_ID=32
+VERSION_CODENAME=""
+PLATFORM_ID="platform:f32"
+PRETTY_NAME="Fedora 32 (Thirty Two)"
+ANSI_COLOR="0;34"
+LOGO=fedora-logo-icon
+CPE_NAME="cpe:/o:fedoraproject:fedora:32"
+HOME_URL="https://fedoraproject.org/"
+DOCUMENTATION_URL="https://docs.fedoraproject.org/en-US/fedora/f32/system-administrators-guide/"
+SUPPORT_URL="https://fedoraproject.org/wiki/Communicating_and_getting_help"
+BUG_REPORT_URL="https://bugzilla.redhat.com/"
+REDHAT_BUGZILLA_PRODUCT="Fedora"
+REDHAT_BUGZILLA_PRODUCT_VERSION=32
+REDHAT_SUPPORT_PRODUCT="Fedora"
+REDHAT_SUPPORT_PRODUCT_VERSION=32
+PRIVACY_POLICY_URL="https://fedoraproject.org/wiki/Legal:PrivacyPolicy"
+"""
+
+UBUNTU_OS_RELEASE = """\
+NAME="Ubuntu"
+VERSION="20.04.1 LTS (Focal Fossa)"
+ID=ubuntu
+ID_LIKE=debian
+PRETTY_NAME="Ubuntu 20.04.1 LTS"
+VERSION_ID="20.04"
+HOME_URL="https://www.ubuntu.com/"
+SUPPORT_URL="https://help.ubuntu.com/"
+BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
+PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
+VERSION_CODENAME=focal
+UBUNTU_CODENAME=focal
+"""
+
+TEST_OS_RELEASE = r"""
+# test data
+ID_LIKE="egg spam viking"
+EMPTY=
+# comments and empty lines are ignored
+
+SINGLE_QUOTE='single'
+EMPTY_SINGLE=''
+DOUBLE_QUOTE="double"
+EMPTY_DOUBLE=""
+QUOTES="double\'s"
+SPECIALS="\$\`\\\'\""
+# invalid lines
+=invalid
+=
+INVALID
+IN-VALID=value
+IN VALID=value
+"""
+
class PlatformTest(unittest.TestCase):
def clear_caches(self):
platform._platform_cache.clear()
platform._sys_version_cache.clear()
platform._uname_cache = None
+ platform._os_release_cache = None
def test_architecture(self):
res = platform.architecture()
@@ -382,6 +440,54 @@ class PlatformTest(unittest.TestCase):
self.assertEqual(platform.platform(terse=1), expected_terse)
self.assertEqual(platform.platform(), expected)
+ def test_freedesktop_os_release(self):
+ self.addCleanup(self.clear_caches)
+ self.clear_caches()
+
+ if any(os.path.isfile(fn) for fn in platform._os_release_candidates):
+ info = platform.freedesktop_os_release()
+ self.assertIn("NAME", info)
+ self.assertIn("ID", info)
+
+ info["CPYTHON_TEST"] = "test"
+ self.assertNotIn(
+ "CPYTHON_TEST",
+ platform.freedesktop_os_release()
+ )
+ else:
+ with self.assertRaises(OSError):
+ platform.freedesktop_os_release()
+
+ def test_parse_os_release(self):
+ info = platform._parse_os_release(FEDORA_OS_RELEASE.splitlines())
+ self.assertEqual(info["NAME"], "Fedora")
+ self.assertEqual(info["ID"], "fedora")
+ self.assertNotIn("ID_LIKE", info)
+ self.assertEqual(info["VERSION_CODENAME"], "")
+
+ info = platform._parse_os_release(UBUNTU_OS_RELEASE.splitlines())
+ self.assertEqual(info["NAME"], "Ubuntu")
+ self.assertEqual(info["ID"], "ubuntu")
+ self.assertEqual(info["ID_LIKE"], "debian")
+ self.assertEqual(info["VERSION_CODENAME"], "focal")
+
+ info = platform._parse_os_release(TEST_OS_RELEASE.splitlines())
+ expected = {
+ "ID": "linux",
+ "NAME": "Linux",
+ "PRETTY_NAME": "Linux",
+ "ID_LIKE": "egg spam viking",
+ "EMPTY": "",
+ "DOUBLE_QUOTE": "double",
+ "EMPTY_DOUBLE": "",
+ "SINGLE_QUOTE": "single",
+ "EMPTY_SINGLE": "",
+ "QUOTES": "double's",
+ "SPECIALS": "$`\\'\"",
+ }
+ self.assertEqual(info, expected)
+ self.assertEqual(len(info["SPECIALS"]), 5)
+
if __name__ == '__main__':
unittest.main()