[Python-checkins] bpo-28468: Add platform.freedesktop_os_release() (GH-23492)

vstinner webhook-mailer at python.org
Mon Nov 30 16:35:14 EST 2020


https://github.com/python/cpython/commit/5c73afc36ee6cca41009a510092e1f901c5dc0a0
commit: 5c73afc36ee6cca41009a510092e1f901c5dc0a0
branch: master
author: Christian Heimes <christian at python.org>
committer: vstinner <vstinner at python.org>
date: 2020-11-30T22:34:45+01:00
summary:

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 at python.org>
Co-authored-by: Victor Stinner <vstinner at python.org>

files:
A Misc/NEWS.d/next/Library/2020-11-24-13-18-05.bpo-28468.8Gh2d4.rst
M Doc/library/platform.rst
M Doc/whatsnew/3.10.rst
M Lib/platform.py
M Lib/test/test_platform.py

diff --git a/Doc/library/platform.rst b/Doc/library/platform.rst
index b293adf48e6e3..fc51b5de881cc 100644
--- a/Doc/library/platform.rst
+++ b/Doc/library/platform.rst
@@ -253,3 +253,41 @@ Unix Platforms
    using :program:`gcc`.
 
    The file is read and scanned in chunks of *chunksize* bytes.
+
+
+Linux Platforms
+---------------
+
+.. function:: freedesktop_os_release()
+
+   Get operating system identification from ``os-release`` file and return
+   it as a dict. The ``os-release`` file is a `freedesktop.org standard
+   <https://www.freedesktop.org/software/systemd/man/os-release.html>`_ and
+   is available in most Linux distributions. A noticeable exception is
+   Android and Android-based distributions.
+
+   Raises :exc:`OSError` or subclass when neither ``/etc/os-release`` nor
+   ``/usr/lib/os-release`` can be read.
+
+   On success, the function returns a dictionary where keys and values are
+   strings. Values have their special characters like ``"`` and ``$``
+   unquoted. The fields ``NAME``, ``ID``, and ``PRETTY_NAME`` are always
+   defined according to the standard. All other fields are optional. Vendors
+   may include additional fields.
+
+   Note that fields like ``NAME``, ``VERSION``, and ``VARIANT`` are strings
+   suitable for presentation to users. Programs should use fields like
+   ``ID``, ``ID_LIKE``, ``VERSION_ID``, or ``VARIANT_ID`` to identify
+   Linux distributions.
+
+   Example::
+
+      def get_like_distro():
+          info = platform.freedesktop_os_release()
+          ids = [info["ID"]]
+          if "ID_LIKE" in info:
+              # ids are space separated and ordered by precedence
+              ids.extend(info["ID_LIKE"].split())
+          return ids
+
+  .. versionadded:: 3.10
diff --git a/Doc/whatsnew/3.10.rst b/Doc/whatsnew/3.10.rst
index f96a3bcbca95f..a8f1080a504c7 100644
--- a/Doc/whatsnew/3.10.rst
+++ b/Doc/whatsnew/3.10.rst
@@ -254,6 +254,14 @@ Added negative indexing support to :attr:`PurePath.parents
 <pathlib.PurePath.parents>`.
 (Contributed by Yaroslav Pankovych in :issue:`21041`)
 
+platform
+--------
+
+Added :func:`platform.freedesktop_os_release()` to retrieve operation system
+identification from `freedesktop.org os-release
+<https://www.freedesktop.org/software/systemd/man/os-release.html>`_ standard file.
+(Contributed by Christian Heimes in :issue:`28468`)
+
 py_compile
 ----------
 
diff --git a/Lib/platform.py b/Lib/platform.py
index 0eb5167d584f7..138a974f02bb6 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 1590cd509b95c..2c6fbee8b6ffb 100644
--- a/Lib/test/test_platform.py
+++ b/Lib/test/test_platform.py
@@ -8,12 +8,70 @@
 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 @@ def test_macos(self):
                     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()
diff --git a/Misc/NEWS.d/next/Library/2020-11-24-13-18-05.bpo-28468.8Gh2d4.rst b/Misc/NEWS.d/next/Library/2020-11-24-13-18-05.bpo-28468.8Gh2d4.rst
new file mode 100644
index 0000000000000..b1834065cf047
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2020-11-24-13-18-05.bpo-28468.8Gh2d4.rst
@@ -0,0 +1,2 @@
+Add :func:`platform.freedesktop_os_release` function to parse freedesktop.org
+``os-release`` files.



More information about the Python-checkins mailing list