From 8a997785c976bd1a2896b848087339d7a03c4286 Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Tue, 24 Nov 2020 13:18:57 +0100 Subject: [PATCH 1/6] bpo-28468: Add platform.freedesktop_osrelease Signed-off-by: Christian Heimes --- Doc/library/platform.rst | 23 +++++ Doc/whatsnew/3.10.rst | 8 ++ Lib/platform.py | 53 +++++++++++ Lib/test/test_platform.py | 93 +++++++++++++++++++ .../2020-11-24-13-18-05.bpo-28468.8Gh2d4.rst | 2 + 5 files changed, 179 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2020-11-24-13-18-05.bpo-28468.8Gh2d4.rst diff --git a/Doc/library/platform.rst b/Doc/library/platform.rst index b293adf48e6e330..dcd4b4c272a1d23 100644 --- a/Doc/library/platform.rst +++ b/Doc/library/platform.rst @@ -253,3 +253,26 @@ Unix Platforms using :program:`gcc`. The file is read and scanned in chunks of *chunksize* bytes. + + +Linux Platforms +--------------- + +.. function:: freedesktop_osrelease() + + Get operating system identification from ``os-release`` file and return + it as a dict. The ``os-release`` file is a `freedesktop.org standard + `_ and + supported by majority of Linux distributions. All fields except ``NAME`` + and ``ID`` are optional. If present, the ``ID_LIKE`` parsed and presented + as a tuple of strings. + + 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. Vendors may include additional fields. + + Raises :exc:`OSError` when neither ``/etc/os-release`` nor + ``/usr/lib/os-release`` can be read. + + .. versionadded:: 3.10 diff --git a/Doc/whatsnew/3.10.rst b/Doc/whatsnew/3.10.rst index f3e433abf082839..9cc7067a6f143c4 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 `. (Contributed by Yaroslav Pankovych in :issue:`21041`) +platform +-------- + +Added :func:`platform.freedesktop_osrelease()` to retrieve operation system +identification from `freedesktop.org os-release +`_ standard. +(Contributed by Christian Heimes in :issue:`28468`) + py_compile ---------- diff --git a/Lib/platform.py b/Lib/platform.py index 0eb5167d584f794..0efa017f65a8446 100755 --- a/Lib/platform.py +++ b/Lib/platform.py @@ -1230,6 +1230,59 @@ 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 ") +_osrelease_line = re.compile( + "^(?P[a-zA-Z0-9_]+)=(?P[\"\']?)(?P.*)(?P=quote)$" +) +# /etc takes precedence over /usr/lib +_osrelease_candidates = ("/etc/os-release", "/usr/lib/os-release", ) +_osrelease_cache = None + + +def _parse_osrelease(lines): + # NAME and ID fields are mandatory fields with well-known defaults + # in pratice all Linux distributions override NAME and ID. + info = { + "NAME": "Linux", + "ID": "linux" + } + for line in lines: + mo = _osrelease_line.match(line) + if mo is not None: + info[mo.group('name')] = mo.group('value') + + # ID_LIKE is a space separated field of ids + if 'ID_LIKE' in info: + info['ID_LIKE'] = tuple( + id_like for id_like in info['ID_LIKE'].split(' ') if id_like + ) + return info + + +def freedesktop_osrelease(): + """Return operation system identification from freedesktop.org os-release + """ + global _osrelease_cache + + if _osrelease_cache is None: + for candidate in _osrelease_candidates: + try: + with open(candidate) as f: + _osrelease_cache = _parse_osrelease(f) + break + except OSError as e: + e.__traceback__ = None + _osrelease_cache = e + + if isinstance(_osrelease_cache, Exception): + raise _osrelease_cache + else: + return _osrelease_cache.copy() + + ### Command line interface if __name__ == '__main__': diff --git a/Lib/test/test_platform.py b/Lib/test/test_platform.py index 1590cd509b95c5b..20e2f06fede1528 100644 --- a/Lib/test/test_platform.py +++ b/Lib/test/test_platform.py @@ -8,12 +8,63 @@ from test import support from test.support import os_helper +FEDORA_OSRELEASE = """\ +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_OSRELEASE = """\ +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_OSRELEASE = 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" +""" + class PlatformTest(unittest.TestCase): def clear_caches(self): platform._platform_cache.clear() platform._sys_version_cache.clear() platform._uname_cache = None + platform._osrelease_cache = None def test_architecture(self): res = platform.architecture() @@ -382,6 +433,48 @@ def test_macos(self): self.assertEqual(platform.platform(terse=1), expected_terse) self.assertEqual(platform.platform(), expected) + def test_freedesktop_osrelease(self): + self.addCleanup(self.clear_caches) + self.clear_caches() + + if any(os.path.isfile(fn) for fn in platform._osrelease_candidates): + info = platform.freedesktop_osrelease() + self.assertIn("NAME", info) + self.assertIn("ID", info) + + info["CPYTHON_TEST"] = "test" + self.assertNotIn("CPYTHON_TEST", platform.freedesktop_osrelease()) + else: + with self.assertRaises(OSError): + platform.freedesktop_osrelease() + + def test_parse_osrelease(self): + info = platform._parse_osrelease(FEDORA_OSRELEASE.split("\n")) + self.assertEqual(info["NAME"], "Fedora") + self.assertEqual(info["ID"], "fedora") + self.assertNotIn("ID_LIKE", info) + self.assertEqual(info["VERSION_CODENAME"], "") + + info = platform._parse_osrelease(UBUNTU_OSRELEASE.split("\n")) + 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_osrelease(TEST_OSRELEASE.split("\n")) + expected = { + "ID": "linux", + "NAME": "Linux", + "ID_LIKE": ("egg", "spam", "viking"), + "EMPTY": "", + "DOUBLE_QUOTE": "double", + "EMPTY_DOUBLE": "", + "SINGLE_QUOTE": "single", + "EMPTY_SINGLE": "", + "QUOTES": "double's", + } + self.assertEqual(info, expected) + 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 000000000000000..f12a3c5b4938e36 --- /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_osrelease` function to parse freedesktop.org +``os-release`` files. From ceb9f5df7dbfc93240e1641fda48f381cbde7775 Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Tue, 24 Nov 2020 15:38:48 +0100 Subject: [PATCH 2/6] Address review * change name to freedesktop_os_release * add default PRETTY_NAME * exception handling without refleak * more tests * mention Android has no os-release --- Doc/library/platform.rst | 13 +++-- Doc/whatsnew/3.10.rst | 2 +- Lib/platform.py | 47 +++++++++++-------- Lib/test/test_platform.py | 36 +++++++++----- .../2020-11-24-13-18-05.bpo-28468.8Gh2d4.rst | 2 +- 5 files changed, 61 insertions(+), 39 deletions(-) diff --git a/Doc/library/platform.rst b/Doc/library/platform.rst index dcd4b4c272a1d23..ec7d7e261d3adc4 100644 --- a/Doc/library/platform.rst +++ b/Doc/library/platform.rst @@ -258,21 +258,24 @@ Unix Platforms Linux Platforms --------------- -.. function:: freedesktop_osrelease() +.. 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 `_ and - supported by majority of Linux distributions. All fields except ``NAME`` - and ``ID`` are optional. If present, the ``ID_LIKE`` parsed and presented - as a tuple of strings. + is available in most Linux distributions. A noticeable exception is + Android and Android-based distributions. + + All fields except ``NAME``, ``ID``, and ``PRETTY_NAME`` are optional. + If present, the ``ID_LIKE`` parsed and presented as a tuple of strings. + Comments, empty lines, and invalid lines are silently omitted. 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. Vendors may include additional fields. - Raises :exc:`OSError` when neither ``/etc/os-release`` nor + Raises :exc:`OSError` or subclass when neither ``/etc/os-release`` nor ``/usr/lib/os-release`` can be read. .. versionadded:: 3.10 diff --git a/Doc/whatsnew/3.10.rst b/Doc/whatsnew/3.10.rst index 9cc7067a6f143c4..9e04e8f42ef9e18 100644 --- a/Doc/whatsnew/3.10.rst +++ b/Doc/whatsnew/3.10.rst @@ -257,7 +257,7 @@ Added negative indexing support to :attr:`PurePath.parents platform -------- -Added :func:`platform.freedesktop_osrelease()` to retrieve operation system +Added :func:`platform.freedesktop_os_release()` to retrieve operation system identification from `freedesktop.org os-release `_ standard. (Contributed by Christian Heimes in :issue:`28468`) diff --git a/Lib/platform.py b/Lib/platform.py index 0efa017f65a8446..8c8512017c1f963 100755 --- a/Lib/platform.py +++ b/Lib/platform.py @@ -1234,23 +1234,25 @@ def platform(aliased=0, terse=0): # https://www.freedesktop.org/software/systemd/man/os-release.html # NAME=value with optional quotes (' or ") -_osrelease_line = re.compile( +_os_release_line = re.compile( "^(?P[a-zA-Z0-9_]+)=(?P[\"\']?)(?P.*)(?P=quote)$" ) # /etc takes precedence over /usr/lib -_osrelease_candidates = ("/etc/os-release", "/usr/lib/os-release", ) -_osrelease_cache = None +_os_release_candidates = ("/etc/os-release", "/usr/lib/os-release") +_os_release_cache = None -def _parse_osrelease(lines): - # NAME and ID fields are mandatory fields with well-known defaults - # in pratice all Linux distributions override NAME and ID. +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" + "ID": "linux", + "PRETTY_NAME": "Linux", } + for line in lines: - mo = _osrelease_line.match(line) + mo = _os_release_line.match(line) if mo is not None: info[mo.group('name')] = mo.group('value') @@ -1259,28 +1261,35 @@ def _parse_osrelease(lines): info['ID_LIKE'] = tuple( id_like for id_like in info['ID_LIKE'].split(' ') if id_like ) + return info -def freedesktop_osrelease(): +def freedesktop_os_release(candidates=_os_release_candidates): """Return operation system identification from freedesktop.org os-release """ - global _osrelease_cache + global _os_release_cache - if _osrelease_cache is None: - for candidate in _osrelease_candidates: + errno = None + + if _os_release_cache is None: + for candidate in candidates: try: - with open(candidate) as f: - _osrelease_cache = _parse_osrelease(f) + with open(candidate, encoding="utf-8") as f: + _os_release_cache = _parse_os_release(f) break except OSError as e: - e.__traceback__ = None - _osrelease_cache = e + errno = e.errno + else: + _os_release_cache = OSError( + errno, + f"Unable to read files {', '.join(candidates)}" + ) - if isinstance(_osrelease_cache, Exception): - raise _osrelease_cache + if isinstance(_os_release_cache, Exception): + raise _os_release_cache from None else: - return _osrelease_cache.copy() + return _os_release_cache.copy() ### Command line interface diff --git a/Lib/test/test_platform.py b/Lib/test/test_platform.py index 20e2f06fede1528..0aef09c1c74f3ef 100644 --- a/Lib/test/test_platform.py +++ b/Lib/test/test_platform.py @@ -8,7 +8,7 @@ from test import support from test.support import os_helper -FEDORA_OSRELEASE = """\ +FEDORA_os_release = """\ NAME=Fedora VERSION="32 (Thirty Two)" ID=fedora @@ -30,7 +30,7 @@ PRIVACY_POLICY_URL="https://fedoraproject.org/wiki/Legal:PrivacyPolicy" """ -UBUNTU_OSRELEASE = """\ +UBUNTU_os_release = """\ NAME="Ubuntu" VERSION="20.04.1 LTS (Focal Fossa)" ID=ubuntu @@ -45,7 +45,7 @@ UBUNTU_CODENAME=focal """ -TEST_OSRELEASE = r""" +TEST_os_release = r""" # test data ID_LIKE=egg spam viking EMPTY= @@ -56,6 +56,12 @@ DOUBLE_QUOTE="double" EMPTY_DOUBLE="" QUOTES="double's" +# invalid lines +=invalid += +INVALID +IN-VALID=value +IN VALID=value """ @@ -64,7 +70,7 @@ def clear_caches(self): platform._platform_cache.clear() platform._sys_version_cache.clear() platform._uname_cache = None - platform._osrelease_cache = None + platform._os_release_cache = None def test_architecture(self): res = platform.architecture() @@ -433,38 +439,42 @@ def test_macos(self): self.assertEqual(platform.platform(terse=1), expected_terse) self.assertEqual(platform.platform(), expected) - def test_freedesktop_osrelease(self): + def test_freedesktop_os_release(self): self.addCleanup(self.clear_caches) self.clear_caches() - if any(os.path.isfile(fn) for fn in platform._osrelease_candidates): - info = platform.freedesktop_osrelease() + 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_osrelease()) + self.assertNotIn( + "CPYTHON_TEST", + platform.freedesktop_os_release() + ) else: with self.assertRaises(OSError): - platform.freedesktop_osrelease() + platform.freedesktop_os_release() - def test_parse_osrelease(self): - info = platform._parse_osrelease(FEDORA_OSRELEASE.split("\n")) + 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_osrelease(UBUNTU_OSRELEASE.split("\n")) + 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_osrelease(TEST_OSRELEASE.split("\n")) + 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", 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 index f12a3c5b4938e36..b1834065cf047e9 100644 --- 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 @@ -1,2 +1,2 @@ -Add :func:`platform.freedesktop_osrelease` function to parse freedesktop.org +Add :func:`platform.freedesktop_os_release` function to parse freedesktop.org ``os-release`` files. From 8c0550e3de283ae616c2dd44ef1b52b595cefbc5 Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Tue, 24 Nov 2020 16:00:03 +0100 Subject: [PATCH 3/6] Update Doc/whatsnew/3.10.rst Co-authored-by: Victor Stinner --- Doc/whatsnew/3.10.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/whatsnew/3.10.rst b/Doc/whatsnew/3.10.rst index 9e04e8f42ef9e18..43cf1d541ed1a2b 100644 --- a/Doc/whatsnew/3.10.rst +++ b/Doc/whatsnew/3.10.rst @@ -259,7 +259,7 @@ platform Added :func:`platform.freedesktop_os_release()` to retrieve operation system identification from `freedesktop.org os-release -`_ standard. +`_ standard file. (Contributed by Christian Heimes in :issue:`28468`) py_compile From 0fc7d0349bff560bc035ca0a3ac9941e8cbd6e2b Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Tue, 24 Nov 2020 17:09:05 +0100 Subject: [PATCH 4/6] Only cache on success --- Lib/platform.py | 21 +++++++++------------ Lib/test/test_platform.py | 14 +++++++------- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/Lib/platform.py b/Lib/platform.py index 8c8512017c1f963..fa2d9d2843b0678 100755 --- a/Lib/platform.py +++ b/Lib/platform.py @@ -1233,12 +1233,13 @@ def platform(aliased=0, terse=0): ### freedesktop.org os-release standard # https://www.freedesktop.org/software/systemd/man/os-release.html -# NAME=value with optional quotes (' or ") +# 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[a-zA-Z0-9_]+)=(?P[\"\']?)(?P.*)(?P=quote)$" ) # /etc takes precedence over /usr/lib -_os_release_candidates = ("/etc/os-release", "/usr/lib/os-release") +_os_release_candidates = ("/etc/os-release", "/usr/lib/os-relesase") _os_release_cache = None @@ -1265,15 +1266,14 @@ def _parse_os_release(lines): return info -def freedesktop_os_release(candidates=_os_release_candidates): +def freedesktop_os_release(): """Return operation system identification from freedesktop.org os-release """ global _os_release_cache - errno = None - if _os_release_cache is None: - for candidate in candidates: + errno = None + for candidate in _os_release_candidates: try: with open(candidate, encoding="utf-8") as f: _os_release_cache = _parse_os_release(f) @@ -1281,15 +1281,12 @@ def freedesktop_os_release(candidates=_os_release_candidates): except OSError as e: errno = e.errno else: - _os_release_cache = OSError( + raise OSError( errno, - f"Unable to read files {', '.join(candidates)}" + f"Unable to read files {', '.join(_os_release_candidates)}" ) - if isinstance(_os_release_cache, Exception): - raise _os_release_cache from None - else: - return _os_release_cache.copy() + return _os_release_cache.copy() ### Command line interface diff --git a/Lib/test/test_platform.py b/Lib/test/test_platform.py index 0aef09c1c74f3ef..349b5fc56f47ef6 100644 --- a/Lib/test/test_platform.py +++ b/Lib/test/test_platform.py @@ -8,7 +8,7 @@ from test import support from test.support import os_helper -FEDORA_os_release = """\ +FEDORA_OS_RELEASE = """\ NAME=Fedora VERSION="32 (Thirty Two)" ID=fedora @@ -30,7 +30,7 @@ PRIVACY_POLICY_URL="https://fedoraproject.org/wiki/Legal:PrivacyPolicy" """ -UBUNTU_os_release = """\ +UBUNTU_OS_RELEASE = """\ NAME="Ubuntu" VERSION="20.04.1 LTS (Focal Fossa)" ID=ubuntu @@ -45,9 +45,9 @@ UBUNTU_CODENAME=focal """ -TEST_os_release = r""" +TEST_OS_ = r""" # test data -ID_LIKE=egg spam viking +ID_LIKE="egg spam viking" EMPTY= # comments and empty lines are ignored @@ -458,19 +458,19 @@ def test_freedesktop_os_release(self): platform.freedesktop_os_release() def test_parse_os_release(self): - info = platform._parse_os_release(FEDORA_os_release.splitlines()) + 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()) + 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()) + info = platform._parse_os_release(TEST_OS_RELEASE.splitlines()) expected = { "ID": "linux", "NAME": "Linux", From 0ed04eacb1844a3e2d5de3c927aea4a94d6972b2 Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Tue, 24 Nov 2020 18:49:07 +0100 Subject: [PATCH 5/6] Unquote 5 backslash quoted characters --- Lib/platform.py | 6 +++++- Lib/test/test_platform.py | 7 +++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Lib/platform.py b/Lib/platform.py index fa2d9d2843b0678..193ff80442f5857 100755 --- a/Lib/platform.py +++ b/Lib/platform.py @@ -1238,6 +1238,8 @@ def platform(aliased=0, terse=0): _os_release_line = re.compile( "^(?P[a-zA-Z0-9_]+)=(?P[\"\']?)(?P.*)(?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 @@ -1255,7 +1257,9 @@ def _parse_os_release(lines): for line in lines: mo = _os_release_line.match(line) if mo is not None: - info[mo.group('name')] = mo.group('value') + info[mo.group('name')] = _os_release_unescape.sub( + r"\1", mo.group('value') + ) # ID_LIKE is a space separated field of ids if 'ID_LIKE' in info: diff --git a/Lib/test/test_platform.py b/Lib/test/test_platform.py index 349b5fc56f47ef6..1e9ceda7ff940fc 100644 --- a/Lib/test/test_platform.py +++ b/Lib/test/test_platform.py @@ -45,7 +45,7 @@ UBUNTU_CODENAME=focal """ -TEST_OS_ = r""" +TEST_OS_RELEASE = r""" # test data ID_LIKE="egg spam viking" EMPTY= @@ -55,7 +55,8 @@ EMPTY_SINGLE='' DOUBLE_QUOTE="double" EMPTY_DOUBLE="" -QUOTES="double's" +QUOTES="double\'s" +SPECIALS="\$\`\\\'\"" # invalid lines =invalid = @@ -482,8 +483,10 @@ def test_parse_os_release(self): "SINGLE_QUOTE": "single", "EMPTY_SINGLE": "", "QUOTES": "double's", + "SPECIALS": "$`\\'\"", } self.assertEqual(info, expected) + self.assertEqual(len(info["SPECIALS"]), 5) if __name__ == '__main__': From 679af65ca3798dc36dedabfdc66303b4cee49373 Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Wed, 25 Nov 2020 12:38:42 +0100 Subject: [PATCH 6/6] Victor prefers to keep ID_LIKE untouched Documentation now shows how to use ID_LIKE correctly. --- Doc/library/platform.rst | 28 ++++++++++++++++++++-------- Lib/platform.py | 6 ------ Lib/test/test_platform.py | 4 ++-- 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/Doc/library/platform.rst b/Doc/library/platform.rst index ec7d7e261d3adc4..fc51b5de881cc42 100644 --- a/Doc/library/platform.rst +++ b/Doc/library/platform.rst @@ -266,16 +266,28 @@ Linux Platforms is available in most Linux distributions. A noticeable exception is Android and Android-based distributions. - All fields except ``NAME``, ``ID``, and ``PRETTY_NAME`` are optional. - If present, the ``ID_LIKE`` parsed and presented as a tuple of strings. - Comments, empty lines, and invalid lines are silently omitted. + 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. Vendors may include additional fields. - - Raises :exc:`OSError` or subclass when neither ``/etc/os-release`` nor - ``/usr/lib/os-release`` can be read. + ``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/Lib/platform.py b/Lib/platform.py index 193ff80442f5857..138a974f02bb6db 100755 --- a/Lib/platform.py +++ b/Lib/platform.py @@ -1261,12 +1261,6 @@ def _parse_os_release(lines): r"\1", mo.group('value') ) - # ID_LIKE is a space separated field of ids - if 'ID_LIKE' in info: - info['ID_LIKE'] = tuple( - id_like for id_like in info['ID_LIKE'].split(' ') if id_like - ) - return info diff --git a/Lib/test/test_platform.py b/Lib/test/test_platform.py index 1e9ceda7ff940fc..2c6fbee8b6ffb53 100644 --- a/Lib/test/test_platform.py +++ b/Lib/test/test_platform.py @@ -468,7 +468,7 @@ def test_parse_os_release(self): 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["ID_LIKE"], "debian") self.assertEqual(info["VERSION_CODENAME"], "focal") info = platform._parse_os_release(TEST_OS_RELEASE.splitlines()) @@ -476,7 +476,7 @@ def test_parse_os_release(self): "ID": "linux", "NAME": "Linux", "PRETTY_NAME": "Linux", - "ID_LIKE": ("egg", "spam", "viking"), + "ID_LIKE": "egg spam viking", "EMPTY": "", "DOUBLE_QUOTE": "double", "EMPTY_DOUBLE": "",