diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ef9f0751..c78c1112 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,9 +1,10 @@ repos: - repo: https://github.com/psf/black - rev: 21.6b0 + rev: 21.12b0 hooks: - id: black args: ["--target-version", "py27"] + additional_dependencies: ["click==7.1.2"] - repo: https://github.com/PyCQA/flake8 rev: 3.9.2 hooks: diff --git a/dev-requirements.txt b/dev-requirements.txt index 0645a666..0ef58b9d 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -3,4 +3,5 @@ pytest-cov sphinx>=1.1 pre-commit==2.13.0; python_version > '3.5' black; python_version > '3.5' and platform_python_implementation != 'PyPy' +click==7.1.2 mypy; python_version > '3.5' and platform_python_implementation != 'PyPy' diff --git a/distro.py b/distro.py index 5d298f63..a54e54a0 100644 --- a/distro.py +++ b/distro.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python # Copyright 2015,2016,2017 Nir Cohen # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -85,6 +86,7 @@ #: * Value: Normalized value. NORMALIZED_OS_ID = { "ol": "oracle", # Oracle Linux + "opensuse-leap": "opensuse", # Newer versions of OpenSuSE report as opensuse-leap } #: Translation table for normalizing the "Distributor ID" attribute returned by @@ -151,7 +153,8 @@ def linux_distribution(full_distribution_name=True): * ``version``: The result of :func:`distro.version`. - * ``codename``: The result of :func:`distro.codename`. + * ``codename``: The extra item (usually in parentheses) after the + os-release version number, or the result of :func:`distro.codename`. The interface of this function is compatible with the original :py:func:`platform.linux_distribution` function, supporting a subset of @@ -198,8 +201,9 @@ def id(): "fedora" Fedora "sles" SUSE Linux Enterprise Server "opensuse" openSUSE - "amazon" Amazon Linux + "amzn" Amazon Linux "arch" Arch Linux + "buildroot" Buildroot "cloudlinux" CloudLinux OS "exherbo" Exherbo Linux "gentoo" GenToo Linux @@ -219,6 +223,8 @@ def id(): "netbsd" NetBSD "freebsd" FreeBSD "midnightbsd" MidnightBSD + "rocky" Rocky Linux + "guix" Guix System ============== ========================================= If you have a need to get distros for reliable IDs added into this set, @@ -313,6 +319,10 @@ def version(pretty=False, best=False): sources in a fixed priority order does not always yield the most precise version (e.g. for Debian 8.2, or CentOS 7.1). + Some other distributions may not provide this kind of information. In these + cases, an empty string would be returned. This behavior can be observed + with rolling releases distributions (e.g. Arch Linux). + The *best* parameter can be used to control the approach for the returned version: @@ -723,10 +733,6 @@ def __init__( * :py:exc:`IOError`: Some I/O issue with an os-release file or distro release file. - * :py:exc:`subprocess.CalledProcessError`: The lsb_release command had - some issue (other than not being available in the program execution - path). - * :py:exc:`UnicodeError`: A data source has unexpected characters or uses an unexpected encoding. """ @@ -784,7 +790,7 @@ def linux_distribution(self, full_distribution_name=True): return ( self.name() if full_distribution_name else self.id(), self.version(), - self.codename(), + self._os_release_info.get("release_codename") or self.codename(), ) def id(self): @@ -860,6 +866,13 @@ def version(self, pretty=False, best=False): ).get("version_id", ""), self.uname_attr("release"), ] + if self.id() == "debian" or "debian" in self.like().split(): + # On Debian-like, add debian_version file content to candidates list. + try: + with open(os.path.join(self.etc_dir, "debian_version")) as fp: + versions.append(fp.readline().rstrip()) + except IOError: + pass version = "" if best: # This algorithm uses the last version in priority order that has @@ -1102,12 +1115,17 @@ def _parse_os_release_content(lines): # stripped, etc.), so the tokens are now either: # * variable assignments: var=value # * commands or their arguments (not allowed in os-release) + # Ignore any tokens that are not variable assignments if "=" in token: k, v = token.split("=", 1) props[k.lower()] = v - else: - # Ignore any tokens that are not variable assignments - pass + + if "version" in props: + # extract release codename (if any) from version attribute + match = re.search(r"\((\D+)\)|,\s*(\D+)", props["version"]) + if match: + release_codename = match.group(1) or match.group(2) + props["codename"] = props["release_codename"] = release_codename if "version_codename" in props: # os-release added a version_codename field. Use that in @@ -1118,16 +1136,6 @@ def _parse_os_release_content(lines): elif "ubuntu_codename" in props: # Same as above but a non-standard field name used on older Ubuntus props["codename"] = props["ubuntu_codename"] - elif "version" in props: - # If there is no version_codename, parse it from the version - match = re.search(r"(\(\D+\))|,(\s+)?\D+", props["version"]) - if match: - codename = match.group() - codename = codename.strip("()") - codename = codename.strip(",") - codename = codename.strip() - # codename appears within paranthese. - props["codename"] = codename return props @@ -1276,6 +1284,7 @@ def _distro_release_info(self): "manjaro-release", "oracle-release", "redhat-release", + "rocky-release", "sl-release", "slackware-version", ] diff --git a/tests/resources/distros/buildroot/etc/os-release b/tests/resources/distros/buildroot/etc/os-release new file mode 100644 index 00000000..3aeb5317 --- /dev/null +++ b/tests/resources/distros/buildroot/etc/os-release @@ -0,0 +1,5 @@ +NAME=Buildroot +VERSION=2022.02 +ID=buildroot +VERSION_ID=2022.02 +PRETTY_NAME="Buildroot 2022.02" diff --git a/tests/resources/distros/debian10/bin/lsb_release b/tests/resources/distros/debian10/bin/lsb_release new file mode 100644 index 00000000..7d269dcc --- /dev/null +++ b/tests/resources/distros/debian10/bin/lsb_release @@ -0,0 +1,21 @@ +#!/bin/bash +# +# lsb_release command for testing the ld module. +# Only the -a option is supported. +# +# This version of the lsb_release command works without a corresponding +# etc/lsb-release file. +# + +if [[ "$@" != "-a" ]]; then + echo "Usage: lsb_release -a" + exit 2 +fi + +echo "No LSB modules are available." +echo "Distributor ID: Debian" +echo "Description: Debian GNU/Linux 10 (buster)" +echo "Release: 10" +echo "Codename: buster" + +exit 0 diff --git a/tests/resources/distros/debian10/etc/debian_version b/tests/resources/distros/debian10/etc/debian_version new file mode 100644 index 00000000..7f0c091e --- /dev/null +++ b/tests/resources/distros/debian10/etc/debian_version @@ -0,0 +1 @@ +10.11 diff --git a/tests/resources/distros/debian10/etc/os-release b/tests/resources/distros/debian10/etc/os-release new file mode 100644 index 00000000..9b5419df --- /dev/null +++ b/tests/resources/distros/debian10/etc/os-release @@ -0,0 +1,9 @@ +PRETTY_NAME="Debian GNU/Linux 10 (buster)" +NAME="Debian GNU/Linux" +VERSION_ID="10" +VERSION="10 (buster)" +VERSION_CODENAME=buster +ID=debian +HOME_URL="https://www.debian.org/" +SUPPORT_URL="https://www.debian.org/support" +BUG_REPORT_URL="https://bugs.debian.org/" diff --git a/tests/resources/distros/guix/etc/os-release b/tests/resources/distros/guix/etc/os-release new file mode 100644 index 00000000..8aa5b73d --- /dev/null +++ b/tests/resources/distros/guix/etc/os-release @@ -0,0 +1,8 @@ +NAME="Guix System" +ID=guix +PRETTY_NAME="Guix System" +LOGO=guix-icon +HOME_URL="https://guix.gnu.org" +DOCUMENTATION_URL="https://guix.gnu.org/en/manual" +SUPPORT_URL="https://guix.gnu.org/en/help" +BUG_REPORT_URL="https://lists.gnu.org/mailman/listinfo/bug-guix" diff --git a/tests/resources/distros/opensuse15/etc/os-release b/tests/resources/distros/opensuse15/etc/os-release new file mode 100644 index 00000000..1494bc55 --- /dev/null +++ b/tests/resources/distros/opensuse15/etc/os-release @@ -0,0 +1,10 @@ +NAME="openSUSE Leap" +VERSION="15.2" +ID="opensuse-leap" +ID_LIKE="suse opensuse" +VERSION_ID="15.2" +PRETTY_NAME="openSUSE Leap 15.2" +ANSI_COLOR="0;32" +CPE_NAME="cpe:/o:opensuse:leap:15.2" +BUG_REPORT_URL="https://bugs.opensuse.org" +HOME_URL="https://www.opensuse.org/" diff --git a/tests/resources/distros/rocky/etc/centos-release b/tests/resources/distros/rocky/etc/centos-release new file mode 120000 index 00000000..780b78a5 --- /dev/null +++ b/tests/resources/distros/rocky/etc/centos-release @@ -0,0 +1 @@ +rocky-release \ No newline at end of file diff --git a/tests/resources/distros/rocky/etc/os-release b/tests/resources/distros/rocky/etc/os-release new file mode 120000 index 00000000..c4c75b41 --- /dev/null +++ b/tests/resources/distros/rocky/etc/os-release @@ -0,0 +1 @@ +../usr/lib/os-release \ No newline at end of file diff --git a/tests/resources/distros/rocky/etc/redhat-release b/tests/resources/distros/rocky/etc/redhat-release new file mode 120000 index 00000000..780b78a5 --- /dev/null +++ b/tests/resources/distros/rocky/etc/redhat-release @@ -0,0 +1 @@ +rocky-release \ No newline at end of file diff --git a/tests/resources/distros/rocky/etc/rocky-release b/tests/resources/distros/rocky/etc/rocky-release new file mode 100644 index 00000000..b7323215 --- /dev/null +++ b/tests/resources/distros/rocky/etc/rocky-release @@ -0,0 +1 @@ +Rocky Linux release 8.4 (Green Obsidian) diff --git a/tests/resources/distros/rocky/etc/rocky-release-upstream b/tests/resources/distros/rocky/etc/rocky-release-upstream new file mode 100644 index 00000000..5d7b2828 --- /dev/null +++ b/tests/resources/distros/rocky/etc/rocky-release-upstream @@ -0,0 +1 @@ +Derived from Red Hat Enterprise Linux 8.4 diff --git a/tests/resources/distros/rocky/etc/system-release b/tests/resources/distros/rocky/etc/system-release new file mode 120000 index 00000000..780b78a5 --- /dev/null +++ b/tests/resources/distros/rocky/etc/system-release @@ -0,0 +1 @@ +rocky-release \ No newline at end of file diff --git a/tests/resources/distros/rocky/etc/system-release-cpe b/tests/resources/distros/rocky/etc/system-release-cpe new file mode 100644 index 00000000..741db580 --- /dev/null +++ b/tests/resources/distros/rocky/etc/system-release-cpe @@ -0,0 +1 @@ +cpe:/o:rocky:rocky:8.4:GA diff --git a/tests/resources/distros/rocky/usr/lib/os-release b/tests/resources/distros/rocky/usr/lib/os-release new file mode 100644 index 00000000..f753b059 --- /dev/null +++ b/tests/resources/distros/rocky/usr/lib/os-release @@ -0,0 +1,13 @@ +NAME="Rocky Linux" +VERSION="8.4 (Green Obsidian)" +ID="rocky" +ID_LIKE="rhel centos fedora" +VERSION_ID="8.4" +PLATFORM_ID="platform:el8" +PRETTY_NAME="Rocky Linux 8.4 (Green Obsidian)" +ANSI_COLOR="0;32" +CPE_NAME="cpe:/o:rocky:rocky:8.4:GA" +HOME_URL="https://rockylinux.org/" +BUG_REPORT_URL="https://bugs.rockylinux.org/" +ROCKY_SUPPORT_PRODUCT="Rocky Linux" +ROCKY_SUPPORT_PRODUCT_VERSION="8" diff --git a/tests/test_distro.py b/tests/test_distro.py index 212357be..9ae43b6f 100644 --- a/tests/test_distro.py +++ b/tests/test_distro.py @@ -139,11 +139,10 @@ def _setup_for_distro(self, distro_root): class TestOSRelease: def setup_method(self, test_method): dist = test_method.__name__.split("_")[1] - os_release = os.path.join(DISTROS_DIR, dist, "etc", "os-release") self.distro = distro.LinuxDistribution( include_lsb=False, - os_release_file=os_release, distro_release_file="path-to-non-existing-file", + root_dir=os.path.join(DISTROS_DIR, dist), ) def _test_outcome(self, outcome): @@ -164,6 +163,17 @@ def test_arch_os_release(self): } self._test_outcome(desired_outcome) + def test_buildroot_os_release(self): + desired_outcome = { + "id": "buildroot", + "name": "Buildroot", + "pretty_name": "Buildroot 2022.02", + "version": "2022.02", + "pretty_version": "2022.02", + "best_version": "2022.02", + } + self._test_outcome(desired_outcome) + def test_kali_os_release(self): desired_outcome = { "id": "kali", @@ -207,11 +217,23 @@ def test_debian8_os_release(self): "pretty_name": "Debian GNU/Linux 8 (jessie)", "version": "8", "pretty_version": "8 (jessie)", - "best_version": "8", + "best_version": "8.2", "codename": "jessie", } self._test_outcome(desired_outcome) + def test_debian10_os_release(self): + desired_outcome = { + "id": "debian", + "name": "Debian GNU/Linux", + "pretty_name": "Debian GNU/Linux 10 (buster)", + "version": "10", + "pretty_version": "10 (buster)", + "best_version": "10.11", + "codename": "buster", + } + self._test_outcome(desired_outcome) + def test_fedora19_os_release(self): desired_outcome = { "id": "fedora", @@ -252,6 +274,14 @@ def test_fedora30_os_release(self): } self._test_outcome(desired_outcome) + def test_guix_os_release(self): + desired_outcome = { + "id": "guix", + "name": "Guix System", + "pretty_name": "Guix System", + } + self._test_outcome(desired_outcome) + def test_kvmibm1_os_release(self): desired_outcome = { "id": "kvmibm", @@ -309,6 +339,18 @@ def test_opensuse42_os_release(self): } self._test_outcome(desired_outcome) + def test_opensuse15_os_release(self): + desired_outcome = { + "id": "opensuse", + "name": "openSUSE Leap", + "pretty_name": "openSUSE Leap 15.2", + "version": "15.2", + "pretty_version": "15.2", + "best_version": "15.2", + "like": "suse opensuse", + } + self._test_outcome(desired_outcome) + def test_raspbian7_os_release(self): desired_outcome = { "id": "raspbian", @@ -316,7 +358,7 @@ def test_raspbian7_os_release(self): "pretty_name": "Raspbian GNU/Linux 7 (wheezy)", "version": "7", "pretty_version": "7 (wheezy)", - "best_version": "7", + "best_version": "7.1", "like": "debian", "codename": "wheezy", } @@ -329,7 +371,7 @@ def test_raspbian8_os_release(self): "pretty_name": "Raspbian GNU/Linux 8 (jessie)", "version": "8", "pretty_version": "8 (jessie)", - "best_version": "8", + "best_version": "8.0", "like": "debian", "codename": "jessie", } @@ -348,6 +390,19 @@ def test_rhel7_os_release(self): } self._test_outcome(desired_outcome) + def test_rocky_os_release(self): + desired_outcome = { + "id": "rocky", + "name": "Rocky Linux", + "pretty_name": "Rocky Linux 8.4 (Green Obsidian)", + "version": "8.4", + "pretty_version": "8.4 (Green Obsidian)", + "best_version": "8.4", + "like": "rhel centos fedora", + "codename": "Green Obsidian", + } + self._test_outcome(desired_outcome) + def test_slackware14_os_release(self): desired_outcome = { "id": "slackware", @@ -480,7 +535,6 @@ def setup_method(self, test_method): dist = test_method.__name__.split("_")[1] self._setup_for_distro(os.path.join(DISTROS_DIR, dist)) self.distro = distro.LinuxDistribution( - include_lsb=True, os_release_file="path-to-non-existing-file", distro_release_file="path-to-non-existing-file", ) @@ -586,7 +640,6 @@ def test_ubuntu14normal_lsb_release(self): self._setup_for_distro(os.path.join(TESTDISTROS, "lsb", "ubuntu14_normal")) self.distro = distro.LinuxDistribution( - include_lsb=True, os_release_file="path-to-non-existing-file", distro_release_file="path-to-non-existing-file", ) @@ -606,7 +659,6 @@ def test_ubuntu14nomodules_lsb_release(self): self._setup_for_distro(os.path.join(TESTDISTROS, "lsb", "ubuntu14_nomodules")) self.distro = distro.LinuxDistribution( - include_lsb=True, os_release_file="path-to-non-existing-file", distro_release_file="path-to-non-existing-file", ) @@ -628,7 +680,6 @@ def test_trailingblanks_lsb_release(self): ) self.distro = distro.LinuxDistribution( - include_lsb=True, os_release_file="path-to-non-existing-file", distro_release_file="path-to-non-existing-file", ) @@ -651,7 +702,6 @@ def test_lsb_release_error_level(self, errnum): ) lsb_release_info = distro.LinuxDistribution( - include_lsb=True, os_release_file="path-to-non-existing-file", distro_release_file="path-to-non-existing-file", )._lsb_release_info @@ -1049,7 +1099,6 @@ class TestOverall(DistroTestCase): TODO: This class should have testcases for all distros that are claimed to be reliably maintained w.r.t. to their ID (see `id()`). Testcases for the following distros are still missing: - * `amazon` - Amazon Linux * `gentoo` - GenToo Linux * `ibm_powerkvm` - IBM PowerKVM * `parallels` - Parallels @@ -1178,6 +1227,20 @@ def test_debian8_release(self): self._test_outcome(desired_outcome) self._test_non_existing_release_file() + def test_debian10_release(self): + desired_outcome = { + "id": "debian", + "name": "Debian GNU/Linux", + "pretty_name": "Debian GNU/Linux 10 (buster)", + "version": "10", + "pretty_version": "10 (buster)", + "best_version": "10.11", + "codename": "buster", + "major_version": "10", + } + self._test_outcome(desired_outcome) + self._test_non_existing_release_file() + def test_exherbo_release(self): desired_outcome = { "id": "exherbo", @@ -1354,6 +1417,20 @@ def test_opensuse42_release(self): } self._test_release_file_info("SuSE-release", desired_info) + def test_opensuse15_release(self): + desired_outcome = { + "id": "opensuse", + "name": "openSUSE Leap", + "pretty_name": "openSUSE Leap 15.2", + "version": "15.2", + "pretty_version": "15.2", + "best_version": "15.2", + "like": "suse opensuse", + "major_version": "15", + "minor_version": "2", + } + self._test_outcome(desired_outcome) + def test_oracle7_release(self): desired_outcome = { "id": "oracle", @@ -1382,7 +1459,7 @@ def test_raspbian7_release(self): "pretty_name": "Raspbian GNU/Linux 7 (wheezy)", "version": "7", "pretty_version": "7 (wheezy)", - "best_version": "7", + "best_version": "7.1", "like": "debian", "codename": "wheezy", "major_version": "7", @@ -1397,7 +1474,7 @@ def test_raspbian8_release(self): "pretty_name": "Raspbian GNU/Linux 8 (jessie)", "version": "8", "pretty_version": "8 (jessie)", - "best_version": "8", + "best_version": "8.0", "like": "debian", "codename": "jessie", "major_version": "8", @@ -1472,6 +1549,29 @@ def test_rhel7_release(self): } self._test_release_file_info("redhat-release", desired_info) + def test_rocky_release(self): + desired_outcome = { + "id": "rocky", + "name": "Rocky Linux", + "pretty_name": "Rocky Linux 8.4 (Green Obsidian)", + "version": "8.4", + "pretty_version": "8.4 (Green Obsidian)", + "best_version": "8.4", + "like": "rhel centos fedora", + "codename": "Green Obsidian", + "major_version": "8", + "minor_version": "4", + } + self._test_outcome(desired_outcome) + + desired_info = { + "id": "centos", + "name": "Rocky Linux", + "version_id": "8.4", + "codename": "Green Obsidian", + } + self._test_release_file_info("centos-release", desired_info) + def test_slackware14_release(self): desired_outcome = { "id": "slackware", @@ -1772,6 +1872,9 @@ def setup_method(self, test_method): self.ubuntu14_os_release = os.path.join( DISTROS_DIR, "ubuntu14", "etc", "os-release" ) + self.fedora30_os_release = os.path.join( + DISTROS_DIR, "fedora30", "etc", "os-release" + ) def test_info(self): _distro = distro.LinuxDistribution( @@ -1845,6 +1948,12 @@ def test_linux_distribution(self): assert i == ("Ubuntu", "14.04", "Trusty Tahr") def test_linux_distribution_full_false(self): + _distro = distro.LinuxDistribution( + include_lsb=False, os_release_file=self.fedora30_os_release + ) + i = _distro.linux_distribution() + assert i == ("Fedora", "30", "Thirty") + _distro = distro.LinuxDistribution( include_lsb=False, os_release_file=self.ubuntu14_os_release ) diff --git a/tox.ini b/tox.ini index fed6d5e4..9aec2cf4 100644 --- a/tox.ini +++ b/tox.ini @@ -21,7 +21,7 @@ skip_missing_interpreters = true deps = pytest pytest-cov -commands = pytest --cov-report term-missing --cov distro +commands = pytest --cov-report term-missing --cov distro {posargs} [testenv:lint] deps = pre-commit