-
-
Notifications
You must be signed in to change notification settings - Fork 32.5k
bpo-28468: Add platform.freedesktop_osrelease #23492
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
8a99778
ceb9f5d
8c0550e
0fc7d03
0ed04ea
679af65
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
Signed-off-by: Christian Heimes <christian@python.org>
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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<name>[a-zA-Z0-9_]+)=(?P<quote>[\"\']?)(?P<value>.*)(?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" | ||
tiran marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
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 | ||
) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suggest to remove this code. I'm not sure if it's worth it to have a special case only for the ID_LIKE variable. The platform module is mostly based on strings. You introduce a special case where a single variable is a tuple. Since it's well specified that it's space separated, the caller can split the string by themselves, no? Splitting at spaces it trivial in Python:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the only special case that a lot of users will get wrong and that won't get detected by CI. For almost all distros the Users always have to split the field and always check for "name in info["ID_LIKE"].split()". The explicit handling of ID_LIKE get rids of a foot gun. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The problem is that if tomorrow a new field is added with a value separated by spaces, the caller might expect Python to magically split it as a tuple. But now, we will have to update the function whenever a new field is added for add a special case. If there is no special case and it's a dummy parser, the code is more likely to stay for the same for the next 10 years. I would prefer to not reintroduce the same problem that we fixed by removing linux_distribution(): having to update the stdlib when the filename changes or file content evolves. If you want a more advanced view of the os-release, I suggest to write a higher level API on top of it and put it on PyPI, so it can be easily updated (similar to the distro module which contains many special cases). |
||
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: | ||
tiran marked this conversation as resolved.
Show resolved
Hide resolved
|
||
_osrelease_cache = _parse_osrelease(f) | ||
break | ||
except OSError as e: | ||
e.__traceback__ = None | ||
_osrelease_cache = e | ||
tiran marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
if isinstance(_osrelease_cache, Exception): | ||
raise _osrelease_cache | ||
else: | ||
return _osrelease_cache.copy() | ||
|
||
|
||
### Command line interface | ||
|
||
if __name__ == '__main__': | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,12 +8,63 @@ | |
from test import support | ||
from test.support import os_helper | ||
|
||
FEDORA_OSRELEASE = """\ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The trailing \ is surprising. Is it needed? Maybe write """...""".lstrip()? |
||
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" | ||
tiran marked this conversation as resolved.
Show resolved
Hide resolved
|
||
""" | ||
|
||
|
||
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")) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remark: For the purpose of the unit test, .splitlines() would also work, and it's less strict, no? |
||
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() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
Add :func:`platform.freedesktop_osrelease` function to parse freedesktop.org | ||
``os-release`` files. |
Uh oh!
There was an error while loading. Please reload this page.