Source code for extra_platforms.platform

# Copyright Kevin Deldycke <kevin@deldycke.com> and contributors.
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
"""Platforms, also known as Operating Systems."""

from __future__ import annotations

import platform
from dataclasses import dataclass, field

import distro

from .trait import Trait

TYPE_CHECKING = False
if TYPE_CHECKING:
    from typing import Any


_MACOS_CODENAMES = {
    ("10", "0"): "Cheetah",
    ("10", "1"): "Puma",
    ("10", "2"): "Jaguar",
    ("10", "3"): "Panther",
    ("10", "4"): "Tiger",
    ("10", "5"): "Leopard",
    ("10", "6"): "Snow Leopard",
    ("10", "7"): "Lion",
    ("10", "8"): "Mountain Lion",
    ("10", "9"): "Mavericks",
    ("10", "10"): "Yosemite",
    ("10", "11"): "El Capitan",
    ("10", "12"): "Sierra",
    ("10", "13"): "High Sierra",
    ("10", "14"): "Mojave",
    ("10", "15"): "Catalina",
    ("11", None): "Big Sur",
    ("12", None): "Monterey",
    ("13", None): "Ventura",
    ("14", None): "Sonoma",
    ("15", None): "Sequoia",
    ("26", None): "Tahoe",
}
"""Maps macOS ``(major, minor)`` version parts to release code name.

See:

- https://en.wikipedia.org/wiki/Template:MacOS_versions
- https://docs.python.org/3/library/platform.html#platform.mac_ver
"""


def _get_macos_codename(major: str | None = None, minor: str | None = None) -> str:
    matches = set()
    for (major_key, minor_key), codename in _MACOS_CODENAMES.items():
        if minor_key is not None and minor_key != minor:
            continue
        if major_key == major:
            matches.add(codename)
    if not matches:
        raise ValueError(f"No macOS codename match version ({major!r}, {minor!r})")
    if len(matches) != 1:
        raise ValueError(
            f"Version {major}.{minor} match multiple codenames: {matches!r}"
        )
    return matches.pop()


def _recursive_update(
    a: dict[str, Any], b: dict[str, Any], strict: bool = False
) -> dict[str, Any]:
    """Like standard ``dict.update()``, but recursive so sub-dict gets updated.

    Ignore elements present in ``b`` but not in ``a``. Unless ``strict`` is set to
    ``True``, in which case a ``ValueError`` exception will be raised.
    """
    for k, v in b.items():
        if isinstance(v, dict) and isinstance(a.get(k), dict):
            a[k] = _recursive_update(a[k], v, strict=strict)
        # Ignore elements unregistered in the template structure.
        elif k in a:
            a[k] = b[k]
        elif strict:
            raise ValueError(f"Parameter {k!r} found in second dict but not in first.")
    return a


def _remove_blanks(
    tree: dict,
    remove_none: bool = True,
    remove_dicts: bool = True,
    remove_str: bool = True,
) -> dict:
    """Returns a copy of a dict without items whose values blanks.

    Are considered blanks:

    - ``None`` values
    - empty strings
    - empty ``dict``

    The removal of each of these class can be skipped by setting ``remove_*``
    parameters.

    Dictionarries are inspected recursively and their own blank values are removed.
    """
    result = {}
    for key, value in tree.items():
        # Skip None values if configured.
        if remove_none and value is None:
            continue

        # Recursively process nested dicts.
        if isinstance(value, dict):
            cleaned = _remove_blanks(value, remove_none, remove_dicts, remove_str)
            # Skip empty dicts if configured.
            if remove_dicts and not cleaned:
                continue
            result[key] = cleaned
        # Skip empty strings if configured.
        elif remove_str and isinstance(value, str) and not value:
            continue
        else:
            result[key] = value

    return result


[docs] @dataclass(frozen=True) class Platform(Trait): """A platform can identify multiple distributions or OSes with the same characteristics. It has a unique ID, a human-readable name, and boolean to flag current platform. """ icon: str = field(repr=False, default="❓") """Icon of the platform.""" def __post_init__(self) -> None: """Validate and normalize platform fields.""" super().__post_init__()
[docs] def info(self) -> dict[str, str | bool | None | dict[str, str | None]]: """Returns all platform attributes we can gather.""" info: dict[str, str | bool | None | dict[str, str | None]] = { **self._base_info(), # Extra fields from distro.info(). "distro_id": None, "version": None, "version_parts": {"major": None, "minor": None, "build_number": None}, "like": None, "codename": None, } if self.current: # Get extra Linux distribution info from distro. distro_info = dict(distro.info()) # Rename distro ID to avoid conflict with our own ID. distro_info["distro_id"] = distro_info.pop("id") info = _recursive_update(info, _remove_blanks(distro_info), strict=True) # Add extra macOS infos. if self.id == "macos": info = _recursive_update(info, self._macos_infos(), strict=True) # Add extra Windows infos. elif self.id == "windows": info = _recursive_update(info, self._windows_infos(), strict=True) return info
@staticmethod def _macos_infos() -> dict[str, Any]: """Fetch extra macOS infos. Returns the same dict structure as ``distro.info()``. """ release, _versioninfo, _machine = platform.mac_ver() parts = dict(zip(("major", "minor", "build_number"), release.split(".", 2))) major = parts.get("major") minor = parts.get("minor") build_number = parts.get("build_number") return { "version": release, "version_parts": { "major": major, "minor": minor, "build_number": build_number, }, "codename": _get_macos_codename(major, minor), } @staticmethod def _windows_infos() -> dict[str, Any]: """Fetch extra Windows infos. Returns the same dict structure as ``distro.info()``. .. todo: Get even more details for windows version? See inspirations from: https://github.com/saltstack/salt/blob/246d066/salt/grains/core.py#L1432-L1488 """ release, _version, _csd, _ptype = platform.win32_ver() parts = dict(zip(("major", "minor", "build_number"), release.split(".", 2))) major = parts.get("major") minor = parts.get("minor") build_number = parts.get("build_number") return { "version": release, "version_parts": { "major": major, "minor": minor, "build_number": build_number, }, "codename": " ".join((release, platform.win32_edition())), }