Skip to content

Commit

Permalink
Add support for PEP 730 iOS tags (#832)
Browse files Browse the repository at this point in the history
  • Loading branch information
freakboy3742 authored Oct 2, 2024
1 parent 9cb29bf commit eefade3
Show file tree
Hide file tree
Showing 3 changed files with 182 additions and 5 deletions.
15 changes: 15 additions & 0 deletions docs/tags.rst
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,21 @@ to the implementation to provide.
compatibility


.. function:: ios_platforms(version=None, multiarch=None)

Yields the :attr:`~Tag.platform` tags for iOS.

:param tuple version: A two-item tuple representing the version of iOS.
Defaults to the current system's version.
:param str multiarch: The CPU architecture+ABI to be used. This should be in
the format by ``sys.implementation._multiarch`` (e.g.,
``arm64_iphoneos`` or ``x84_64_iphonesimulator``).
Defaults to the current system's multiarch value.

.. note::
Behavior of this method is undefined if invoked on non-iOS platforms
without providing explicit version and multiarch arguments.

.. function:: platform_tags(version=None, arch=None)

Yields the :attr:`~Tag.platform` tags for the running interpreter.
Expand Down
69 changes: 64 additions & 5 deletions src/packaging/tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
logger = logging.getLogger(__name__)

PythonVersion = Sequence[int]
MacVersion = Tuple[int, int]
AppleVersion = Tuple[int, int]

INTERPRETER_SHORT_NAMES: dict[str, str] = {
"python": "py", # Generic.
Expand Down Expand Up @@ -362,7 +362,7 @@ def _mac_arch(arch: str, is_32bit: bool = _32_BIT_INTERPRETER) -> str:
return "i386"


def _mac_binary_formats(version: MacVersion, cpu_arch: str) -> list[str]:
def _mac_binary_formats(version: AppleVersion, cpu_arch: str) -> list[str]:
formats = [cpu_arch]
if cpu_arch == "x86_64":
if version < (10, 4):
Expand Down Expand Up @@ -395,7 +395,7 @@ def _mac_binary_formats(version: MacVersion, cpu_arch: str) -> list[str]:


def mac_platforms(
version: MacVersion | None = None, arch: str | None = None
version: AppleVersion | None = None, arch: str | None = None
) -> Iterator[str]:
"""
Yields the platform tags for a macOS system.
Expand All @@ -407,7 +407,7 @@ def mac_platforms(
"""
version_str, _, cpu_arch = platform.mac_ver()
if version is None:
version = cast("MacVersion", tuple(map(int, version_str.split(".")[:2])))
version = cast("AppleVersion", tuple(map(int, version_str.split(".")[:2])))
if version == (10, 16):
# When built against an older macOS SDK, Python will report macOS 10.16
# instead of the real version.
Expand All @@ -423,7 +423,7 @@ def mac_platforms(
stdout=subprocess.PIPE,
text=True,
).stdout
version = cast("MacVersion", tuple(map(int, version_str.split(".")[:2])))
version = cast("AppleVersion", tuple(map(int, version_str.split(".")[:2])))
else:
version = version
if arch is None:
Expand Down Expand Up @@ -473,6 +473,63 @@ def mac_platforms(
yield f"macosx_{major_version}_{minor_version}_{binary_format}"


def ios_platforms(
version: AppleVersion | None = None, multiarch: str | None = None
) -> Iterator[str]:
"""
Yields the platform tags for an iOS system.
:param version: A two-item tuple specifying the iOS version to generate
platform tags for. Defaults to the current iOS version.
:param multiarch: The CPU architecture+ABI to generate platform tags for -
(the value used by `sys.implementation._multiarch` e.g.,
`arm64_iphoneos` or `x84_64_iphonesimulator`). Defaults to the current
multiarch value.
"""
if version is None:
# if iOS is the current platform, ios_ver *must* be defined. However,
# it won't exist for CPython versions before 3.13, which causes a mypy
# error.
_, release, _, _ = platform.ios_ver() # type: ignore[attr-defined]
version = cast("AppleVersion", tuple(map(int, release.split(".")[:2])))

if multiarch is None:
multiarch = sys.implementation._multiarch
multiarch = multiarch.replace("-", "_")

ios_platform_template = "ios_{major}_{minor}_{multiarch}"

# Consider any iOS major.minor version from the version requested, down to
# 12.0. 12.0 is the first iOS version that is known to have enough features
# to support CPython. Consider every possible minor release up to X.9. There
# highest the minor has ever gone is 8 (14.8 and 15.8) but having some extra
# candidates that won't ever match doesn't really hurt, and it saves us from
# having to keep an explicit list of known iOS versions in the code. Return
# the results descending order of version number.

# If the requested major version is less than 12, there won't be any matches.
if version[0] < 12:
return

# Consider the actual X.Y version that was requested.
yield ios_platform_template.format(
major=version[0], minor=version[1], multiarch=multiarch
)

# Consider every minor version from X.0 to the minor version prior to the
# version requested by the platform.
for minor in range(version[1] - 1, -1, -1):
yield ios_platform_template.format(
major=version[0], minor=minor, multiarch=multiarch
)

for major in range(version[0] - 1, 11, -1):
for minor in range(9, -1, -1):
yield ios_platform_template.format(
major=major, minor=minor, multiarch=multiarch
)


def _linux_platforms(is_32bit: bool = _32_BIT_INTERPRETER) -> Iterator[str]:
linux = _normalize_string(sysconfig.get_platform())
if not linux.startswith("linux_"):
Expand Down Expand Up @@ -502,6 +559,8 @@ def platform_tags() -> Iterator[str]:
"""
if platform.system() == "Darwin":
return mac_platforms()
elif platform.system() == "iOS":
return ios_platforms()
elif platform.system() == "Linux":
return _linux_platforms()
else:
Expand Down
103 changes: 103 additions & 0 deletions tests/test_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,30 @@ def mock(name):
return mock


@pytest.fixture
def mock_ios(monkeypatch):
# Monkeypatch the platform to be iOS
monkeypatch.setattr(sys, "platform", "ios")

# Mock a fake architecture that will fit the expected pattern, but
# wont actually be a legal multiarch.
monkeypatch.setattr(
sys.implementation,
"_multiarch",
"gothic-iphoneos",
raising=False,
)

# Mock the return value of platform.ios_ver.
def mock_ios_ver(*args):
return ("iOS", "13.2", "iPhone15,2", False)

if sys.version_info < (3, 13):
platform.ios_ver = mock_ios_ver
else:
monkeypatch.setattr(platform, "ios_ver", mock_ios_ver)


class TestTag:
def test_lowercasing(self):
tag = tags.Tag("PY3", "None", "ANY")
Expand Down Expand Up @@ -335,6 +359,84 @@ def test_macos_11(self, major, minor):
assert "macosx_12_0_universal2" in platforms


class TestIOSPlatforms:
def test_version_detection(self, mock_ios):
platforms = list(tags.ios_platforms(multiarch="arm64-iphoneos"))
assert platforms == [
"ios_13_2_arm64_iphoneos",
"ios_13_1_arm64_iphoneos",
"ios_13_0_arm64_iphoneos",
"ios_12_9_arm64_iphoneos",
"ios_12_8_arm64_iphoneos",
"ios_12_7_arm64_iphoneos",
"ios_12_6_arm64_iphoneos",
"ios_12_5_arm64_iphoneos",
"ios_12_4_arm64_iphoneos",
"ios_12_3_arm64_iphoneos",
"ios_12_2_arm64_iphoneos",
"ios_12_1_arm64_iphoneos",
"ios_12_0_arm64_iphoneos",
]

def test_multiarch_detection(self, mock_ios):
platforms = list(tags.ios_platforms(version=(12, 0)))
assert platforms == ["ios_12_0_gothic_iphoneos"]

def test_ios_platforms(self, mock_ios):
# Pre-iOS 12.0 releases won't match anything
platforms = list(tags.ios_platforms((7, 0), "arm64-iphoneos"))
assert platforms == []

# iOS 12.0 returns exactly 1 match
platforms = list(tags.ios_platforms((12, 0), "arm64-iphoneos"))
assert platforms == ["ios_12_0_arm64_iphoneos"]

# iOS 13.0 returns a match for 13.0, plus every 12.X
platforms = list(tags.ios_platforms((13, 0), "x86_64-iphonesimulator"))
assert platforms == [
"ios_13_0_x86_64_iphonesimulator",
"ios_12_9_x86_64_iphonesimulator",
"ios_12_8_x86_64_iphonesimulator",
"ios_12_7_x86_64_iphonesimulator",
"ios_12_6_x86_64_iphonesimulator",
"ios_12_5_x86_64_iphonesimulator",
"ios_12_4_x86_64_iphonesimulator",
"ios_12_3_x86_64_iphonesimulator",
"ios_12_2_x86_64_iphonesimulator",
"ios_12_1_x86_64_iphonesimulator",
"ios_12_0_x86_64_iphonesimulator",
]

# iOS 14.3 returns a match for 14.3-14.0, plus every 13.X and every 12.X
platforms = list(tags.ios_platforms((14, 3), "arm64-iphoneos"))
assert platforms == [
"ios_14_3_arm64_iphoneos",
"ios_14_2_arm64_iphoneos",
"ios_14_1_arm64_iphoneos",
"ios_14_0_arm64_iphoneos",
"ios_13_9_arm64_iphoneos",
"ios_13_8_arm64_iphoneos",
"ios_13_7_arm64_iphoneos",
"ios_13_6_arm64_iphoneos",
"ios_13_5_arm64_iphoneos",
"ios_13_4_arm64_iphoneos",
"ios_13_3_arm64_iphoneos",
"ios_13_2_arm64_iphoneos",
"ios_13_1_arm64_iphoneos",
"ios_13_0_arm64_iphoneos",
"ios_12_9_arm64_iphoneos",
"ios_12_8_arm64_iphoneos",
"ios_12_7_arm64_iphoneos",
"ios_12_6_arm64_iphoneos",
"ios_12_5_arm64_iphoneos",
"ios_12_4_arm64_iphoneos",
"ios_12_3_arm64_iphoneos",
"ios_12_2_arm64_iphoneos",
"ios_12_1_arm64_iphoneos",
"ios_12_0_arm64_iphoneos",
]


class TestManylinuxPlatform:
def teardown_method(self):
# Clear the version cache
Expand Down Expand Up @@ -619,6 +721,7 @@ def test_linux_not_linux(self, monkeypatch):
"platform_name,dispatch_func",
[
("Darwin", "mac_platforms"),
("iOS", "ios_platforms"),
("Linux", "_linux_platforms"),
("Generic", "_generic_platforms"),
],
Expand Down

0 comments on commit eefade3

Please sign in to comment.