refactor(pypi): split out code for env marker evaluation for reuse (#2068)

This is just a small PR to reduce the scope of #2059.

This just moves some code from one python file to a separate one.

Work towards #260, #1105, #1868.
diff --git a/python/private/pypi/whl_installer/BUILD.bazel b/python/private/pypi/whl_installer/BUILD.bazel
index fc9c0e6..5bce1a5 100644
--- a/python/private/pypi/whl_installer/BUILD.bazel
+++ b/python/private/pypi/whl_installer/BUILD.bazel
@@ -5,6 +5,7 @@
     srcs = [
         "arguments.py",
         "namespace_pkgs.py",
+        "platform.py",
         "wheel.py",
         "wheel_installer.py",
     ],
diff --git a/python/private/pypi/whl_installer/arguments.py b/python/private/pypi/whl_installer/arguments.py
index 173d3a3..29bea80 100644
--- a/python/private/pypi/whl_installer/arguments.py
+++ b/python/private/pypi/whl_installer/arguments.py
@@ -17,7 +17,7 @@
 import pathlib
 from typing import Any, Dict, Set
 
-from python.private.pypi.whl_installer import wheel
+from python.private.pypi.whl_installer.platform import Platform
 
 
 def parser(**kwargs: Any) -> argparse.ArgumentParser:
@@ -44,7 +44,7 @@
     parser.add_argument(
         "--platform",
         action="extend",
-        type=wheel.Platform.from_string,
+        type=Platform.from_string,
         help="Platforms to target dependencies. Can be used multiple times.",
     )
     parser.add_argument(
diff --git a/python/private/pypi/whl_installer/platform.py b/python/private/pypi/whl_installer/platform.py
new file mode 100644
index 0000000..83e42b0
--- /dev/null
+++ b/python/private/pypi/whl_installer/platform.py
@@ -0,0 +1,302 @@
+# Copyright 2024 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Utility class to inspect an extracted wheel directory"""
+
+import platform
+import sys
+from dataclasses import dataclass
+from enum import Enum
+from typing import Any, Dict, Iterator, List, Optional, Union
+
+
+class OS(Enum):
+    linux = 1
+    osx = 2
+    windows = 3
+    darwin = osx
+    win32 = windows
+
+    @classmethod
+    def interpreter(cls) -> "OS":
+        "Return the interpreter operating system."
+        return cls[sys.platform.lower()]
+
+    def __str__(self) -> str:
+        return self.name.lower()
+
+
+class Arch(Enum):
+    x86_64 = 1
+    x86_32 = 2
+    aarch64 = 3
+    ppc = 4
+    s390x = 5
+    arm = 6
+    amd64 = x86_64
+    arm64 = aarch64
+    i386 = x86_32
+    i686 = x86_32
+    x86 = x86_32
+    ppc64le = ppc
+
+    @classmethod
+    def interpreter(cls) -> "Arch":
+        "Return the currently running interpreter architecture."
+        # FIXME @aignas 2023-12-13: Hermetic toolchain on Windows 3.11.6
+        # is returning an empty string here, so lets default to x86_64
+        return cls[platform.machine().lower() or "x86_64"]
+
+    def __str__(self) -> str:
+        return self.name.lower()
+
+
+def _as_int(value: Optional[Union[OS, Arch]]) -> int:
+    """Convert one of the enums above to an int for easier sorting algorithms.
+
+    Args:
+        value: The value of an enum or None.
+
+    Returns:
+        -1 if we get None, otherwise, the numeric value of the given enum.
+    """
+    if value is None:
+        return -1
+
+    return int(value.value)
+
+
+def host_interpreter_minor_version() -> int:
+    return sys.version_info.minor
+
+
+@dataclass(frozen=True)
+class Platform:
+    os: Optional[OS] = None
+    arch: Optional[Arch] = None
+    minor_version: Optional[int] = None
+
+    @classmethod
+    def all(
+        cls,
+        want_os: Optional[OS] = None,
+        minor_version: Optional[int] = None,
+    ) -> List["Platform"]:
+        return sorted(
+            [
+                cls(os=os, arch=arch, minor_version=minor_version)
+                for os in OS
+                for arch in Arch
+                if not want_os or want_os == os
+            ]
+        )
+
+    @classmethod
+    def host(cls) -> List["Platform"]:
+        """Use the Python interpreter to detect the platform.
+
+        We extract `os` from sys.platform and `arch` from platform.machine
+
+        Returns:
+            A list of parsed values which makes the signature the same as
+            `Platform.all` and `Platform.from_string`.
+        """
+        return [
+            Platform(
+                os=OS.interpreter(),
+                arch=Arch.interpreter(),
+                minor_version=host_interpreter_minor_version(),
+            )
+        ]
+
+    def all_specializations(self) -> Iterator["Platform"]:
+        """Return the platform itself and all its unambiguous specializations.
+
+        For more info about specializations see
+        https://bazel.build/docs/configurable-attributes
+        """
+        yield self
+        if self.arch is None:
+            for arch in Arch:
+                yield Platform(os=self.os, arch=arch, minor_version=self.minor_version)
+        if self.os is None:
+            for os in OS:
+                yield Platform(os=os, arch=self.arch, minor_version=self.minor_version)
+        if self.arch is None and self.os is None:
+            for os in OS:
+                for arch in Arch:
+                    yield Platform(os=os, arch=arch, minor_version=self.minor_version)
+
+    def __lt__(self, other: Any) -> bool:
+        """Add a comparison method, so that `sorted` returns the most specialized platforms first."""
+        if not isinstance(other, Platform) or other is None:
+            raise ValueError(f"cannot compare {other} with Platform")
+
+        self_arch, self_os = _as_int(self.arch), _as_int(self.os)
+        other_arch, other_os = _as_int(other.arch), _as_int(other.os)
+
+        if self_os == other_os:
+            return self_arch < other_arch
+        else:
+            return self_os < other_os
+
+    def __str__(self) -> str:
+        if self.minor_version is None:
+            if self.os is None and self.arch is None:
+                return "//conditions:default"
+
+            if self.arch is None:
+                return f"@platforms//os:{self.os}"
+            else:
+                return f"{self.os}_{self.arch}"
+
+        if self.arch is None and self.os is None:
+            return f"@//python/config_settings:is_python_3.{self.minor_version}"
+
+        if self.arch is None:
+            return f"cp3{self.minor_version}_{self.os}_anyarch"
+
+        if self.os is None:
+            return f"cp3{self.minor_version}_anyos_{self.arch}"
+
+        return f"cp3{self.minor_version}_{self.os}_{self.arch}"
+
+    @classmethod
+    def from_string(cls, platform: Union[str, List[str]]) -> List["Platform"]:
+        """Parse a string and return a list of platforms"""
+        platform = [platform] if isinstance(platform, str) else list(platform)
+        ret = set()
+        for p in platform:
+            if p == "host":
+                ret.update(cls.host())
+                continue
+
+            abi, _, tail = p.partition("_")
+            if not abi.startswith("cp"):
+                # The first item is not an abi
+                tail = p
+                abi = ""
+            os, _, arch = tail.partition("_")
+            arch = arch or "*"
+
+            minor_version = int(abi[len("cp3") :]) if abi else None
+
+            if arch != "*":
+                ret.add(
+                    cls(
+                        os=OS[os] if os != "*" else None,
+                        arch=Arch[arch],
+                        minor_version=minor_version,
+                    )
+                )
+
+            else:
+                ret.update(
+                    cls.all(
+                        want_os=OS[os] if os != "*" else None,
+                        minor_version=minor_version,
+                    )
+                )
+
+        return sorted(ret)
+
+    # NOTE @aignas 2023-12-05: below is the minimum number of accessors that are defined in
+    # https://peps.python.org/pep-0496/ to make rules_python generate dependencies.
+    #
+    # WARNING: It may not work in cases where the python implementation is different between
+    # different platforms.
+
+    # derived from OS
+    @property
+    def os_name(self) -> str:
+        if self.os == OS.linux or self.os == OS.osx:
+            return "posix"
+        elif self.os == OS.windows:
+            return "nt"
+        else:
+            return ""
+
+    @property
+    def sys_platform(self) -> str:
+        if self.os == OS.linux:
+            return "linux"
+        elif self.os == OS.osx:
+            return "darwin"
+        elif self.os == OS.windows:
+            return "win32"
+        else:
+            return ""
+
+    @property
+    def platform_system(self) -> str:
+        if self.os == OS.linux:
+            return "Linux"
+        elif self.os == OS.osx:
+            return "Darwin"
+        elif self.os == OS.windows:
+            return "Windows"
+        else:
+            return ""
+
+    # derived from OS and Arch
+    @property
+    def platform_machine(self) -> str:
+        """Guess the target 'platform_machine' marker.
+
+        NOTE @aignas 2023-12-05: this may not work on really new systems, like
+        Windows if they define the platform markers in a different way.
+        """
+        if self.arch == Arch.x86_64:
+            return "x86_64"
+        elif self.arch == Arch.x86_32 and self.os != OS.osx:
+            return "i386"
+        elif self.arch == Arch.x86_32:
+            return ""
+        elif self.arch == Arch.aarch64 and self.os == OS.linux:
+            return "aarch64"
+        elif self.arch == Arch.aarch64:
+            # Assuming that OSX and Windows use this one since the precedent is set here:
+            # https://github.com/cgohlke/win_arm64-wheels
+            return "arm64"
+        elif self.os != OS.linux:
+            return ""
+        elif self.arch == Arch.ppc64le:
+            return "ppc64le"
+        elif self.arch == Arch.s390x:
+            return "s390x"
+        else:
+            return ""
+
+    def env_markers(self, extra: str) -> Dict[str, str]:
+        # If it is None, use the host version
+        minor_version = self.minor_version or host_interpreter_minor_version()
+
+        return {
+            "extra": extra,
+            "os_name": self.os_name,
+            "sys_platform": self.sys_platform,
+            "platform_machine": self.platform_machine,
+            "platform_system": self.platform_system,
+            "platform_release": "",  # unset
+            "platform_version": "",  # unset
+            "python_version": f"3.{minor_version}",
+            # FIXME @aignas 2024-01-14: is putting zero last a good idea? Maybe we should
+            # use `20` or something else to avoid having weird issues where the full version is used for
+            # matching and the author decides to only support 3.y.5 upwards.
+            "implementation_version": f"3.{minor_version}.0",
+            "python_full_version": f"3.{minor_version}.0",
+            # we assume that the following are the same as the interpreter used to setup the deps:
+            # "implementation_name": "cpython"
+            # "platform_python_implementation: "CPython",
+        }
diff --git a/python/private/pypi/whl_installer/wheel.py b/python/private/pypi/whl_installer/wheel.py
index c167df9..0f6bd27 100644
--- a/python/private/pypi/whl_installer/wheel.py
+++ b/python/private/pypi/whl_installer/wheel.py
@@ -15,299 +15,20 @@
 """Utility class to inspect an extracted wheel directory"""
 
 import email
-import platform
 import re
-import sys
 from collections import defaultdict
 from dataclasses import dataclass
-from enum import Enum
 from pathlib import Path
-from typing import Any, Dict, Iterator, List, Optional, Set, Tuple, Union
+from typing import Dict, List, Optional, Set, Tuple
 
 import installer
 from packaging.requirements import Requirement
 from pip._vendor.packaging.utils import canonicalize_name
 
-
-class OS(Enum):
-    linux = 1
-    osx = 2
-    windows = 3
-    darwin = osx
-    win32 = windows
-
-    @classmethod
-    def interpreter(cls) -> "OS":
-        "Return the interpreter operating system."
-        return cls[sys.platform.lower()]
-
-    def __str__(self) -> str:
-        return self.name.lower()
-
-
-class Arch(Enum):
-    x86_64 = 1
-    x86_32 = 2
-    aarch64 = 3
-    ppc = 4
-    s390x = 5
-    arm = 6
-    amd64 = x86_64
-    arm64 = aarch64
-    i386 = x86_32
-    i686 = x86_32
-    x86 = x86_32
-    ppc64le = ppc
-
-    @classmethod
-    def interpreter(cls) -> "Arch":
-        "Return the currently running interpreter architecture."
-        # FIXME @aignas 2023-12-13: Hermetic toolchain on Windows 3.11.6
-        # is returning an empty string here, so lets default to x86_64
-        return cls[platform.machine().lower() or "x86_64"]
-
-    def __str__(self) -> str:
-        return self.name.lower()
-
-
-def _as_int(value: Optional[Union[OS, Arch]]) -> int:
-    """Convert one of the enums above to an int for easier sorting algorithms.
-
-    Args:
-        value: The value of an enum or None.
-
-    Returns:
-        -1 if we get None, otherwise, the numeric value of the given enum.
-    """
-    if value is None:
-        return -1
-
-    return int(value.value)
-
-
-def host_interpreter_minor_version() -> int:
-    return sys.version_info.minor
-
-
-@dataclass(frozen=True)
-class Platform:
-    os: Optional[OS] = None
-    arch: Optional[Arch] = None
-    minor_version: Optional[int] = None
-
-    @classmethod
-    def all(
-        cls,
-        want_os: Optional[OS] = None,
-        minor_version: Optional[int] = None,
-    ) -> List["Platform"]:
-        return sorted(
-            [
-                cls(os=os, arch=arch, minor_version=minor_version)
-                for os in OS
-                for arch in Arch
-                if not want_os or want_os == os
-            ]
-        )
-
-    @classmethod
-    def host(cls) -> List["Platform"]:
-        """Use the Python interpreter to detect the platform.
-
-        We extract `os` from sys.platform and `arch` from platform.machine
-
-        Returns:
-            A list of parsed values which makes the signature the same as
-            `Platform.all` and `Platform.from_string`.
-        """
-        return [
-            Platform(
-                os=OS.interpreter(),
-                arch=Arch.interpreter(),
-                minor_version=host_interpreter_minor_version(),
-            )
-        ]
-
-    def all_specializations(self) -> Iterator["Platform"]:
-        """Return the platform itself and all its unambiguous specializations.
-
-        For more info about specializations see
-        https://bazel.build/docs/configurable-attributes
-        """
-        yield self
-        if self.arch is None:
-            for arch in Arch:
-                yield Platform(os=self.os, arch=arch, minor_version=self.minor_version)
-        if self.os is None:
-            for os in OS:
-                yield Platform(os=os, arch=self.arch, minor_version=self.minor_version)
-        if self.arch is None and self.os is None:
-            for os in OS:
-                for arch in Arch:
-                    yield Platform(os=os, arch=arch, minor_version=self.minor_version)
-
-    def __lt__(self, other: Any) -> bool:
-        """Add a comparison method, so that `sorted` returns the most specialized platforms first."""
-        if not isinstance(other, Platform) or other is None:
-            raise ValueError(f"cannot compare {other} with Platform")
-
-        self_arch, self_os = _as_int(self.arch), _as_int(self.os)
-        other_arch, other_os = _as_int(other.arch), _as_int(other.os)
-
-        if self_os == other_os:
-            return self_arch < other_arch
-        else:
-            return self_os < other_os
-
-    def __str__(self) -> str:
-        if self.minor_version is None:
-            if self.os is None and self.arch is None:
-                return "//conditions:default"
-
-            if self.arch is None:
-                return f"@platforms//os:{self.os}"
-            else:
-                return f"{self.os}_{self.arch}"
-
-        if self.arch is None and self.os is None:
-            return f"@//python/config_settings:is_python_3.{self.minor_version}"
-
-        if self.arch is None:
-            return f"cp3{self.minor_version}_{self.os}_anyarch"
-
-        if self.os is None:
-            return f"cp3{self.minor_version}_anyos_{self.arch}"
-
-        return f"cp3{self.minor_version}_{self.os}_{self.arch}"
-
-    @classmethod
-    def from_string(cls, platform: Union[str, List[str]]) -> List["Platform"]:
-        """Parse a string and return a list of platforms"""
-        platform = [platform] if isinstance(platform, str) else list(platform)
-        ret = set()
-        for p in platform:
-            if p == "host":
-                ret.update(cls.host())
-                continue
-
-            abi, _, tail = p.partition("_")
-            if not abi.startswith("cp"):
-                # The first item is not an abi
-                tail = p
-                abi = ""
-            os, _, arch = tail.partition("_")
-            arch = arch or "*"
-
-            minor_version = int(abi[len("cp3") :]) if abi else None
-
-            if arch != "*":
-                ret.add(
-                    cls(
-                        os=OS[os] if os != "*" else None,
-                        arch=Arch[arch],
-                        minor_version=minor_version,
-                    )
-                )
-
-            else:
-                ret.update(
-                    cls.all(
-                        want_os=OS[os] if os != "*" else None,
-                        minor_version=minor_version,
-                    )
-                )
-
-        return sorted(ret)
-
-    # NOTE @aignas 2023-12-05: below is the minimum number of accessors that are defined in
-    # https://peps.python.org/pep-0496/ to make rules_python generate dependencies.
-    #
-    # WARNING: It may not work in cases where the python implementation is different between
-    # different platforms.
-
-    # derived from OS
-    @property
-    def os_name(self) -> str:
-        if self.os == OS.linux or self.os == OS.osx:
-            return "posix"
-        elif self.os == OS.windows:
-            return "nt"
-        else:
-            return ""
-
-    @property
-    def sys_platform(self) -> str:
-        if self.os == OS.linux:
-            return "linux"
-        elif self.os == OS.osx:
-            return "darwin"
-        elif self.os == OS.windows:
-            return "win32"
-        else:
-            return ""
-
-    @property
-    def platform_system(self) -> str:
-        if self.os == OS.linux:
-            return "Linux"
-        elif self.os == OS.osx:
-            return "Darwin"
-        elif self.os == OS.windows:
-            return "Windows"
-        else:
-            return ""
-
-    # derived from OS and Arch
-    @property
-    def platform_machine(self) -> str:
-        """Guess the target 'platform_machine' marker.
-
-        NOTE @aignas 2023-12-05: this may not work on really new systems, like
-        Windows if they define the platform markers in a different way.
-        """
-        if self.arch == Arch.x86_64:
-            return "x86_64"
-        elif self.arch == Arch.x86_32 and self.os != OS.osx:
-            return "i386"
-        elif self.arch == Arch.x86_32:
-            return ""
-        elif self.arch == Arch.aarch64 and self.os == OS.linux:
-            return "aarch64"
-        elif self.arch == Arch.aarch64:
-            # Assuming that OSX and Windows use this one since the precedent is set here:
-            # https://github.com/cgohlke/win_arm64-wheels
-            return "arm64"
-        elif self.os != OS.linux:
-            return ""
-        elif self.arch == Arch.ppc64le:
-            return "ppc64le"
-        elif self.arch == Arch.s390x:
-            return "s390x"
-        else:
-            return ""
-
-    def env_markers(self, extra: str) -> Dict[str, str]:
-        # If it is None, use the host version
-        minor_version = self.minor_version or host_interpreter_minor_version()
-
-        return {
-            "extra": extra,
-            "os_name": self.os_name,
-            "sys_platform": self.sys_platform,
-            "platform_machine": self.platform_machine,
-            "platform_system": self.platform_system,
-            "platform_release": "",  # unset
-            "platform_version": "",  # unset
-            "python_version": f"3.{minor_version}",
-            # FIXME @aignas 2024-01-14: is putting zero last a good idea? Maybe we should
-            # use `20` or something else to avoid having weird issues where the full version is used for
-            # matching and the author decides to only support 3.y.5 upwards.
-            "implementation_version": f"3.{minor_version}.0",
-            "python_full_version": f"3.{minor_version}.0",
-            # we assume that the following are the same as the interpreter used to setup the deps:
-            # "implementation_name": "cpython"
-            # "platform_python_implementation: "CPython",
-        }
+from python.private.pypi.whl_installer.platform import (
+    Platform,
+    host_interpreter_minor_version,
+)
 
 
 @dataclass(frozen=True)
diff --git a/tests/pypi/whl_installer/BUILD.bazel b/tests/pypi/whl_installer/BUILD.bazel
index 048a877..e25c4a0 100644
--- a/tests/pypi/whl_installer/BUILD.bazel
+++ b/tests/pypi/whl_installer/BUILD.bazel
@@ -28,6 +28,18 @@
 )
 
 py_test(
+    name = "platform_test",
+    size = "small",
+    srcs = [
+        "platform_test.py",
+    ],
+    data = ["//examples/wheel:minimal_with_py_package"],
+    deps = [
+        ":lib",
+    ],
+)
+
+py_test(
     name = "wheel_installer_test",
     size = "small",
     srcs = [
diff --git a/tests/pypi/whl_installer/platform_test.py b/tests/pypi/whl_installer/platform_test.py
new file mode 100644
index 0000000..7ced1e9
--- /dev/null
+++ b/tests/pypi/whl_installer/platform_test.py
@@ -0,0 +1,152 @@
+import unittest
+from random import shuffle
+
+from python.private.pypi.whl_installer.platform import (
+    OS,
+    Arch,
+    Platform,
+    host_interpreter_minor_version,
+)
+
+
+class MinorVersionTest(unittest.TestCase):
+    def test_host(self):
+        host = host_interpreter_minor_version()
+        self.assertIsNotNone(host)
+
+
+class PlatformTest(unittest.TestCase):
+    def test_can_get_host(self):
+        host = Platform.host()
+        self.assertIsNotNone(host)
+        self.assertEqual(1, len(Platform.from_string("host")))
+        self.assertEqual(host, Platform.from_string("host"))
+
+    def test_can_get_linux_x86_64_without_py_version(self):
+        got = Platform.from_string("linux_x86_64")
+        want = Platform(os=OS.linux, arch=Arch.x86_64)
+        self.assertEqual(want, got[0])
+
+    def test_can_get_specific_from_string(self):
+        got = Platform.from_string("cp33_linux_x86_64")
+        want = Platform(os=OS.linux, arch=Arch.x86_64, minor_version=3)
+        self.assertEqual(want, got[0])
+
+    def test_can_get_all_for_py_version(self):
+        cp39 = Platform.all(minor_version=9)
+        self.assertEqual(18, len(cp39), f"Got {cp39}")
+        self.assertEqual(cp39, Platform.from_string("cp39_*"))
+
+    def test_can_get_all_for_os(self):
+        linuxes = Platform.all(OS.linux, minor_version=9)
+        self.assertEqual(6, len(linuxes))
+        self.assertEqual(linuxes, Platform.from_string("cp39_linux_*"))
+
+    def test_can_get_all_for_os_for_host_python(self):
+        linuxes = Platform.all(OS.linux)
+        self.assertEqual(6, len(linuxes))
+        self.assertEqual(linuxes, Platform.from_string("linux_*"))
+
+    def test_specific_version_specializations(self):
+        any_py33 = Platform(minor_version=3)
+
+        # When
+        all_specializations = list(any_py33.all_specializations())
+
+        want = (
+            [any_py33]
+            + [
+                Platform(arch=arch, minor_version=any_py33.minor_version)
+                for arch in Arch
+            ]
+            + [Platform(os=os, minor_version=any_py33.minor_version) for os in OS]
+            + Platform.all(minor_version=any_py33.minor_version)
+        )
+        self.assertEqual(want, all_specializations)
+
+    def test_aarch64_specializations(self):
+        any_aarch64 = Platform(arch=Arch.aarch64)
+        all_specializations = list(any_aarch64.all_specializations())
+        want = [
+            Platform(os=None, arch=Arch.aarch64),
+            Platform(os=OS.linux, arch=Arch.aarch64),
+            Platform(os=OS.osx, arch=Arch.aarch64),
+            Platform(os=OS.windows, arch=Arch.aarch64),
+        ]
+        self.assertEqual(want, all_specializations)
+
+    def test_linux_specializations(self):
+        any_linux = Platform(os=OS.linux)
+        all_specializations = list(any_linux.all_specializations())
+        want = [
+            Platform(os=OS.linux, arch=None),
+            Platform(os=OS.linux, arch=Arch.x86_64),
+            Platform(os=OS.linux, arch=Arch.x86_32),
+            Platform(os=OS.linux, arch=Arch.aarch64),
+            Platform(os=OS.linux, arch=Arch.ppc),
+            Platform(os=OS.linux, arch=Arch.s390x),
+            Platform(os=OS.linux, arch=Arch.arm),
+        ]
+        self.assertEqual(want, all_specializations)
+
+    def test_osx_specializations(self):
+        any_osx = Platform(os=OS.osx)
+        all_specializations = list(any_osx.all_specializations())
+        # NOTE @aignas 2024-01-14: even though in practice we would only have
+        # Python on osx aarch64 and osx x86_64, we return all arch posibilities
+        # to make the code simpler.
+        want = [
+            Platform(os=OS.osx, arch=None),
+            Platform(os=OS.osx, arch=Arch.x86_64),
+            Platform(os=OS.osx, arch=Arch.x86_32),
+            Platform(os=OS.osx, arch=Arch.aarch64),
+            Platform(os=OS.osx, arch=Arch.ppc),
+            Platform(os=OS.osx, arch=Arch.s390x),
+            Platform(os=OS.osx, arch=Arch.arm),
+        ]
+        self.assertEqual(want, all_specializations)
+
+    def test_platform_sort(self):
+        platforms = [
+            Platform(os=OS.linux, arch=None),
+            Platform(os=OS.linux, arch=Arch.x86_64),
+            Platform(os=OS.osx, arch=None),
+            Platform(os=OS.osx, arch=Arch.x86_64),
+            Platform(os=OS.osx, arch=Arch.aarch64),
+        ]
+        shuffle(platforms)
+        platforms.sort()
+        want = [
+            Platform(os=OS.linux, arch=None),
+            Platform(os=OS.linux, arch=Arch.x86_64),
+            Platform(os=OS.osx, arch=None),
+            Platform(os=OS.osx, arch=Arch.x86_64),
+            Platform(os=OS.osx, arch=Arch.aarch64),
+        ]
+
+        self.assertEqual(want, platforms)
+
+    def test_wheel_os_alias(self):
+        self.assertEqual("osx", str(OS.osx))
+        self.assertEqual(str(OS.darwin), str(OS.osx))
+
+    def test_wheel_arch_alias(self):
+        self.assertEqual("x86_64", str(Arch.x86_64))
+        self.assertEqual(str(Arch.amd64), str(Arch.x86_64))
+
+    def test_wheel_platform_alias(self):
+        give = Platform(
+            os=OS.darwin,
+            arch=Arch.amd64,
+        )
+        alias = Platform(
+            os=OS.osx,
+            arch=Arch.x86_64,
+        )
+
+        self.assertEqual("osx_x86_64", str(give))
+        self.assertEqual(str(alias), str(give))
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/pypi/whl_installer/wheel_test.py b/tests/pypi/whl_installer/wheel_test.py
index 76bfe72..404218e 100644
--- a/tests/pypi/whl_installer/wheel_test.py
+++ b/tests/pypi/whl_installer/wheel_test.py
@@ -1,8 +1,12 @@
 import unittest
-from random import shuffle
 from unittest import mock
 
 from python.private.pypi.whl_installer import wheel
+from python.private.pypi.whl_installer.platform import OS, Arch, Platform
+
+_HOST_INTERPRETER_FN = (
+    "python.private.pypi.whl_installer.wheel.host_interpreter_minor_version"
+)
 
 
 class DepsTest(unittest.TestCase):
@@ -25,10 +29,10 @@
                 "win_dep; os_name=='nt'",
             ],
             platforms={
-                wheel.Platform(os=wheel.OS.linux, arch=wheel.Arch.x86_64),
-                wheel.Platform(os=wheel.OS.osx, arch=wheel.Arch.x86_64),
-                wheel.Platform(os=wheel.OS.osx, arch=wheel.Arch.aarch64),
-                wheel.Platform(os=wheel.OS.windows, arch=wheel.Arch.x86_64),
+                Platform(os=OS.linux, arch=Arch.x86_64),
+                Platform(os=OS.osx, arch=Arch.x86_64),
+                Platform(os=OS.osx, arch=Arch.aarch64),
+                Platform(os=OS.windows, arch=Arch.x86_64),
             },
         )
 
@@ -54,18 +58,10 @@
                 "win_dep; os_name=='nt'",
             ],
             platforms={
-                wheel.Platform(
-                    os=wheel.OS.linux, arch=wheel.Arch.x86_64, minor_version=8
-                ),
-                wheel.Platform(
-                    os=wheel.OS.osx, arch=wheel.Arch.x86_64, minor_version=8
-                ),
-                wheel.Platform(
-                    os=wheel.OS.osx, arch=wheel.Arch.aarch64, minor_version=8
-                ),
-                wheel.Platform(
-                    os=wheel.OS.windows, arch=wheel.Arch.x86_64, minor_version=8
-                ),
+                Platform(os=OS.linux, arch=Arch.x86_64, minor_version=8),
+                Platform(os=OS.osx, arch=Arch.x86_64, minor_version=8),
+                Platform(os=OS.osx, arch=Arch.aarch64, minor_version=8),
+                Platform(os=OS.windows, arch=Arch.x86_64, minor_version=8),
             },
         )
 
@@ -89,8 +85,8 @@
                 "mac_dep; sys_platform=='darwin'",
             ],
             platforms={
-                wheel.Platform(os=wheel.OS.osx, arch=wheel.Arch.x86_64),
-                wheel.Platform(os=wheel.OS.osx, arch=wheel.Arch.aarch64),
+                Platform(os=OS.osx, arch=Arch.x86_64),
+                Platform(os=OS.osx, arch=Arch.aarch64),
             },
         ).build()
 
@@ -113,8 +109,8 @@
                 "m1_dep; sys_platform=='darwin' and platform_machine=='arm64'",
             ],
             platforms={
-                wheel.Platform(os=wheel.OS.osx, arch=wheel.Arch.x86_64),
-                wheel.Platform(os=wheel.OS.osx, arch=wheel.Arch.aarch64),
+                Platform(os=OS.osx, arch=Arch.x86_64),
+                Platform(os=OS.osx, arch=Arch.aarch64),
             },
         ).build()
 
@@ -136,10 +132,10 @@
                 "m1_dep; sys_platform=='darwin' and platform_machine=='arm64'",
             ],
             platforms={
-                wheel.Platform(os=wheel.OS.linux, arch=wheel.Arch.x86_64),
-                wheel.Platform(os=wheel.OS.osx, arch=wheel.Arch.x86_64),
-                wheel.Platform(os=wheel.OS.osx, arch=wheel.Arch.aarch64),
-                wheel.Platform(os=wheel.OS.windows, arch=wheel.Arch.x86_64),
+                Platform(os=OS.linux, arch=Arch.x86_64),
+                Platform(os=OS.osx, arch=Arch.x86_64),
+                Platform(os=OS.osx, arch=Arch.aarch64),
+                Platform(os=OS.windows, arch=Arch.x86_64),
             },
         ).build()
 
@@ -197,18 +193,14 @@
             "foo",
             requires_dist=requires_dist,
             platforms=[
-                wheel.Platform(
-                    os=wheel.OS.linux, arch=wheel.Arch.x86_64, minor_version=8
-                ),
+                Platform(os=OS.linux, arch=Arch.x86_64, minor_version=8),
             ],
         ).build()
         py37_deps = wheel.Deps(
             "foo",
             requires_dist=requires_dist,
             platforms=[
-                wheel.Platform(
-                    os=wheel.OS.linux, arch=wheel.Arch.x86_64, minor_version=7
-                ),
+                Platform(os=OS.linux, arch=Arch.x86_64, minor_version=7),
             ],
         ).build()
 
@@ -217,9 +209,7 @@
         self.assertEqual(["bar"], py38_deps.deps)
         self.assertEqual({"@platforms//os:linux": ["posix_dep"]}, py38_deps.deps_select)
 
-    @mock.patch(
-        "python.private.pypi.whl_installer.wheel.host_interpreter_minor_version"
-    )
+    @mock.patch(_HOST_INTERPRETER_FN)
     def test_no_version_select_when_single_version(self, mock_host_interpreter_version):
         requires_dist = [
             "bar",
@@ -236,9 +226,9 @@
             "foo",
             requires_dist=requires_dist,
             platforms=[
-                wheel.Platform(os=os, arch=wheel.Arch.x86_64, minor_version=minor)
+                Platform(os=os, arch=Arch.x86_64, minor_version=minor)
                 for minor in [8]
-                for os in [wheel.OS.linux, wheel.OS.windows]
+                for os in [OS.linux, OS.windows]
             ],
         )
         got = deps.build()
@@ -253,9 +243,7 @@
             got.deps_select,
         )
 
-    @mock.patch(
-        "python.private.pypi.whl_installer.wheel.host_interpreter_minor_version"
-    )
+    @mock.patch(_HOST_INTERPRETER_FN)
     def test_can_get_version_select(self, mock_host_interpreter_version):
         requires_dist = [
             "bar",
@@ -273,9 +261,9 @@
             "foo",
             requires_dist=requires_dist,
             platforms=[
-                wheel.Platform(os=os, arch=wheel.Arch.x86_64, minor_version=minor)
+                Platform(os=os, arch=Arch.x86_64, minor_version=minor)
                 for minor in [7, 8, 9]
-                for os in [wheel.OS.linux, wheel.OS.windows]
+                for os in [OS.linux, OS.windows]
             ],
         )
         got = deps.build()
@@ -307,9 +295,7 @@
             got.deps_select,
         )
 
-    @mock.patch(
-        "python.private.pypi.whl_installer.wheel.host_interpreter_minor_version"
-    )
+    @mock.patch(_HOST_INTERPRETER_FN)
     def test_deps_spanning_all_target_py_versions_are_added_to_common(
         self, mock_host_version
     ):
@@ -323,16 +309,14 @@
         deps = wheel.Deps(
             "foo",
             requires_dist=requires_dist,
-            platforms=wheel.Platform.from_string(["cp37_*", "cp38_*", "cp39_*"]),
+            platforms=Platform.from_string(["cp37_*", "cp38_*", "cp39_*"]),
         )
         got = deps.build()
 
         self.assertEqual(["bar", "baz"], got.deps)
         self.assertEqual({}, got.deps_select)
 
-    @mock.patch(
-        "python.private.pypi.whl_installer.wheel.host_interpreter_minor_version"
-    )
+    @mock.patch(_HOST_INTERPRETER_FN)
     def test_deps_are_not_duplicated(self, mock_host_version):
         mock_host_version.return_value = 7
 
@@ -352,16 +336,14 @@
         deps = wheel.Deps(
             "foo",
             requires_dist=requires_dist,
-            platforms=wheel.Platform.from_string(["cp37_*", "cp310_*"]),
+            platforms=Platform.from_string(["cp37_*", "cp310_*"]),
         )
         got = deps.build()
 
         self.assertEqual(["bar"], got.deps)
         self.assertEqual({}, got.deps_select)
 
-    @mock.patch(
-        "python.private.pypi.whl_installer.wheel.host_interpreter_minor_version"
-    )
+    @mock.patch(_HOST_INTERPRETER_FN)
     def test_deps_are_not_duplicated_when_encountering_platform_dep_first(
         self, mock_host_version
     ):
@@ -377,7 +359,7 @@
         deps = wheel.Deps(
             "foo",
             requires_dist=requires_dist,
-            platforms=wheel.Platform.from_string(["cp37_*", "cp310_*"]),
+            platforms=Platform.from_string(["cp37_*", "cp310_*"]),
         )
         got = deps.build()
 
@@ -385,149 +367,5 @@
         self.assertEqual({}, got.deps_select)
 
 
-class MinorVersionTest(unittest.TestCase):
-    def test_host(self):
-        host = wheel.host_interpreter_minor_version()
-        self.assertIsNotNone(host)
-
-
-class PlatformTest(unittest.TestCase):
-    def test_can_get_host(self):
-        host = wheel.Platform.host()
-        self.assertIsNotNone(host)
-        self.assertEqual(1, len(wheel.Platform.from_string("host")))
-        self.assertEqual(host, wheel.Platform.from_string("host"))
-
-    def test_can_get_linux_x86_64_without_py_version(self):
-        got = wheel.Platform.from_string("linux_x86_64")
-        want = wheel.Platform(os=wheel.OS.linux, arch=wheel.Arch.x86_64)
-        self.assertEqual(want, got[0])
-
-    def test_can_get_specific_from_string(self):
-        got = wheel.Platform.from_string("cp33_linux_x86_64")
-        want = wheel.Platform(
-            os=wheel.OS.linux, arch=wheel.Arch.x86_64, minor_version=3
-        )
-        self.assertEqual(want, got[0])
-
-    def test_can_get_all_for_py_version(self):
-        cp39 = wheel.Platform.all(minor_version=9)
-        self.assertEqual(18, len(cp39), f"Got {cp39}")
-        self.assertEqual(cp39, wheel.Platform.from_string("cp39_*"))
-
-    def test_can_get_all_for_os(self):
-        linuxes = wheel.Platform.all(wheel.OS.linux, minor_version=9)
-        self.assertEqual(6, len(linuxes))
-        self.assertEqual(linuxes, wheel.Platform.from_string("cp39_linux_*"))
-
-    def test_can_get_all_for_os_for_host_python(self):
-        linuxes = wheel.Platform.all(wheel.OS.linux)
-        self.assertEqual(6, len(linuxes))
-        self.assertEqual(linuxes, wheel.Platform.from_string("linux_*"))
-
-    def test_specific_version_specializations(self):
-        any_py33 = wheel.Platform(minor_version=3)
-
-        # When
-        all_specializations = list(any_py33.all_specializations())
-
-        want = (
-            [any_py33]
-            + [
-                wheel.Platform(arch=arch, minor_version=any_py33.minor_version)
-                for arch in wheel.Arch
-            ]
-            + [
-                wheel.Platform(os=os, minor_version=any_py33.minor_version)
-                for os in wheel.OS
-            ]
-            + wheel.Platform.all(minor_version=any_py33.minor_version)
-        )
-        self.assertEqual(want, all_specializations)
-
-    def test_aarch64_specializations(self):
-        any_aarch64 = wheel.Platform(arch=wheel.Arch.aarch64)
-        all_specializations = list(any_aarch64.all_specializations())
-        want = [
-            wheel.Platform(os=None, arch=wheel.Arch.aarch64),
-            wheel.Platform(os=wheel.OS.linux, arch=wheel.Arch.aarch64),
-            wheel.Platform(os=wheel.OS.osx, arch=wheel.Arch.aarch64),
-            wheel.Platform(os=wheel.OS.windows, arch=wheel.Arch.aarch64),
-        ]
-        self.assertEqual(want, all_specializations)
-
-    def test_linux_specializations(self):
-        any_linux = wheel.Platform(os=wheel.OS.linux)
-        all_specializations = list(any_linux.all_specializations())
-        want = [
-            wheel.Platform(os=wheel.OS.linux, arch=None),
-            wheel.Platform(os=wheel.OS.linux, arch=wheel.Arch.x86_64),
-            wheel.Platform(os=wheel.OS.linux, arch=wheel.Arch.x86_32),
-            wheel.Platform(os=wheel.OS.linux, arch=wheel.Arch.aarch64),
-            wheel.Platform(os=wheel.OS.linux, arch=wheel.Arch.ppc),
-            wheel.Platform(os=wheel.OS.linux, arch=wheel.Arch.s390x),
-            wheel.Platform(os=wheel.OS.linux, arch=wheel.Arch.arm),
-        ]
-        self.assertEqual(want, all_specializations)
-
-    def test_osx_specializations(self):
-        any_osx = wheel.Platform(os=wheel.OS.osx)
-        all_specializations = list(any_osx.all_specializations())
-        # NOTE @aignas 2024-01-14: even though in practice we would only have
-        # Python on osx aarch64 and osx x86_64, we return all arch posibilities
-        # to make the code simpler.
-        want = [
-            wheel.Platform(os=wheel.OS.osx, arch=None),
-            wheel.Platform(os=wheel.OS.osx, arch=wheel.Arch.x86_64),
-            wheel.Platform(os=wheel.OS.osx, arch=wheel.Arch.x86_32),
-            wheel.Platform(os=wheel.OS.osx, arch=wheel.Arch.aarch64),
-            wheel.Platform(os=wheel.OS.osx, arch=wheel.Arch.ppc),
-            wheel.Platform(os=wheel.OS.osx, arch=wheel.Arch.s390x),
-            wheel.Platform(os=wheel.OS.osx, arch=wheel.Arch.arm),
-        ]
-        self.assertEqual(want, all_specializations)
-
-    def test_platform_sort(self):
-        platforms = [
-            wheel.Platform(os=wheel.OS.linux, arch=None),
-            wheel.Platform(os=wheel.OS.linux, arch=wheel.Arch.x86_64),
-            wheel.Platform(os=wheel.OS.osx, arch=None),
-            wheel.Platform(os=wheel.OS.osx, arch=wheel.Arch.x86_64),
-            wheel.Platform(os=wheel.OS.osx, arch=wheel.Arch.aarch64),
-        ]
-        shuffle(platforms)
-        platforms.sort()
-        want = [
-            wheel.Platform(os=wheel.OS.linux, arch=None),
-            wheel.Platform(os=wheel.OS.linux, arch=wheel.Arch.x86_64),
-            wheel.Platform(os=wheel.OS.osx, arch=None),
-            wheel.Platform(os=wheel.OS.osx, arch=wheel.Arch.x86_64),
-            wheel.Platform(os=wheel.OS.osx, arch=wheel.Arch.aarch64),
-        ]
-
-        self.assertEqual(want, platforms)
-
-    def test_wheel_os_alias(self):
-        self.assertEqual("osx", str(wheel.OS.osx))
-        self.assertEqual(str(wheel.OS.darwin), str(wheel.OS.osx))
-
-    def test_wheel_arch_alias(self):
-        self.assertEqual("x86_64", str(wheel.Arch.x86_64))
-        self.assertEqual(str(wheel.Arch.amd64), str(wheel.Arch.x86_64))
-
-    def test_wheel_platform_alias(self):
-        give = wheel.Platform(
-            os=wheel.OS.darwin,
-            arch=wheel.Arch.amd64,
-        )
-        alias = wheel.Platform(
-            os=wheel.OS.osx,
-            arch=wheel.Arch.x86_64,
-        )
-
-        self.assertEqual("osx_x86_64", str(give))
-        self.assertEqual(str(alias), str(give))
-
-
 if __name__ == "__main__":
     unittest.main()