diff options
author | Christian Heimes <christian@python.org> | 2020-11-30 21:34:45 (GMT) |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-11-30 21:34:45 (GMT) |
commit | 5c73afc36ee6cca41009a510092e1f901c5dc0a0 (patch) | |
tree | eb72eecfbbaf3809eead8a972f47064272b34fe3 /Lib | |
parent | 9bdc40ee3e0d886fb62b5334e8a88c1fe9460ba0 (diff) | |
download | cpython-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-x | Lib/platform.py | 57 | ||||
-rw-r--r-- | Lib/test/test_platform.py | 106 |
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() |