Skip to content

Commit eefade3

Browse files
authored
Add support for PEP 730 iOS tags (#832)
1 parent 9cb29bf commit eefade3

File tree

3 files changed

+182
-5
lines changed

3 files changed

+182
-5
lines changed

docs/tags.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,21 @@ to the implementation to provide.
153153
compatibility
154154

155155

156+
.. function:: ios_platforms(version=None, multiarch=None)
157+
158+
Yields the :attr:`~Tag.platform` tags for iOS.
159+
160+
:param tuple version: A two-item tuple representing the version of iOS.
161+
Defaults to the current system's version.
162+
:param str multiarch: The CPU architecture+ABI to be used. This should be in
163+
the format by ``sys.implementation._multiarch`` (e.g.,
164+
``arm64_iphoneos`` or ``x84_64_iphonesimulator``).
165+
Defaults to the current system's multiarch value.
166+
167+
.. note::
168+
Behavior of this method is undefined if invoked on non-iOS platforms
169+
without providing explicit version and multiarch arguments.
170+
156171
.. function:: platform_tags(version=None, arch=None)
157172

158173
Yields the :attr:`~Tag.platform` tags for the running interpreter.

src/packaging/tags.py

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
logger = logging.getLogger(__name__)
2626

2727
PythonVersion = Sequence[int]
28-
MacVersion = Tuple[int, int]
28+
AppleVersion = Tuple[int, int]
2929

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

364364

365-
def _mac_binary_formats(version: MacVersion, cpu_arch: str) -> list[str]:
365+
def _mac_binary_formats(version: AppleVersion, cpu_arch: str) -> list[str]:
366366
formats = [cpu_arch]
367367
if cpu_arch == "x86_64":
368368
if version < (10, 4):
@@ -395,7 +395,7 @@ def _mac_binary_formats(version: MacVersion, cpu_arch: str) -> list[str]:
395395

396396

397397
def mac_platforms(
398-
version: MacVersion | None = None, arch: str | None = None
398+
version: AppleVersion | None = None, arch: str | None = None
399399
) -> Iterator[str]:
400400
"""
401401
Yields the platform tags for a macOS system.
@@ -407,7 +407,7 @@ def mac_platforms(
407407
"""
408408
version_str, _, cpu_arch = platform.mac_ver()
409409
if version is None:
410-
version = cast("MacVersion", tuple(map(int, version_str.split(".")[:2])))
410+
version = cast("AppleVersion", tuple(map(int, version_str.split(".")[:2])))
411411
if version == (10, 16):
412412
# When built against an older macOS SDK, Python will report macOS 10.16
413413
# instead of the real version.
@@ -423,7 +423,7 @@ def mac_platforms(
423423
stdout=subprocess.PIPE,
424424
text=True,
425425
).stdout
426-
version = cast("MacVersion", tuple(map(int, version_str.split(".")[:2])))
426+
version = cast("AppleVersion", tuple(map(int, version_str.split(".")[:2])))
427427
else:
428428
version = version
429429
if arch is None:
@@ -473,6 +473,63 @@ def mac_platforms(
473473
yield f"macosx_{major_version}_{minor_version}_{binary_format}"
474474

475475

476+
def ios_platforms(
477+
version: AppleVersion | None = None, multiarch: str | None = None
478+
) -> Iterator[str]:
479+
"""
480+
Yields the platform tags for an iOS system.
481+
482+
:param version: A two-item tuple specifying the iOS version to generate
483+
platform tags for. Defaults to the current iOS version.
484+
:param multiarch: The CPU architecture+ABI to generate platform tags for -
485+
(the value used by `sys.implementation._multiarch` e.g.,
486+
`arm64_iphoneos` or `x84_64_iphonesimulator`). Defaults to the current
487+
multiarch value.
488+
"""
489+
if version is None:
490+
# if iOS is the current platform, ios_ver *must* be defined. However,
491+
# it won't exist for CPython versions before 3.13, which causes a mypy
492+
# error.
493+
_, release, _, _ = platform.ios_ver() # type: ignore[attr-defined]
494+
version = cast("AppleVersion", tuple(map(int, release.split(".")[:2])))
495+
496+
if multiarch is None:
497+
multiarch = sys.implementation._multiarch
498+
multiarch = multiarch.replace("-", "_")
499+
500+
ios_platform_template = "ios_{major}_{minor}_{multiarch}"
501+
502+
# Consider any iOS major.minor version from the version requested, down to
503+
# 12.0. 12.0 is the first iOS version that is known to have enough features
504+
# to support CPython. Consider every possible minor release up to X.9. There
505+
# highest the minor has ever gone is 8 (14.8 and 15.8) but having some extra
506+
# candidates that won't ever match doesn't really hurt, and it saves us from
507+
# having to keep an explicit list of known iOS versions in the code. Return
508+
# the results descending order of version number.
509+
510+
# If the requested major version is less than 12, there won't be any matches.
511+
if version[0] < 12:
512+
return
513+
514+
# Consider the actual X.Y version that was requested.
515+
yield ios_platform_template.format(
516+
major=version[0], minor=version[1], multiarch=multiarch
517+
)
518+
519+
# Consider every minor version from X.0 to the minor version prior to the
520+
# version requested by the platform.
521+
for minor in range(version[1] - 1, -1, -1):
522+
yield ios_platform_template.format(
523+
major=version[0], minor=minor, multiarch=multiarch
524+
)
525+
526+
for major in range(version[0] - 1, 11, -1):
527+
for minor in range(9, -1, -1):
528+
yield ios_platform_template.format(
529+
major=major, minor=minor, multiarch=multiarch
530+
)
531+
532+
476533
def _linux_platforms(is_32bit: bool = _32_BIT_INTERPRETER) -> Iterator[str]:
477534
linux = _normalize_string(sysconfig.get_platform())
478535
if not linux.startswith("linux_"):
@@ -502,6 +559,8 @@ def platform_tags() -> Iterator[str]:
502559
"""
503560
if platform.system() == "Darwin":
504561
return mac_platforms()
562+
elif platform.system() == "iOS":
563+
return ios_platforms()
505564
elif platform.system() == "Linux":
506565
return _linux_platforms()
507566
else:

tests/test_tags.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,30 @@ def mock(name):
5353
return mock
5454

5555

56+
@pytest.fixture
57+
def mock_ios(monkeypatch):
58+
# Monkeypatch the platform to be iOS
59+
monkeypatch.setattr(sys, "platform", "ios")
60+
61+
# Mock a fake architecture that will fit the expected pattern, but
62+
# wont actually be a legal multiarch.
63+
monkeypatch.setattr(
64+
sys.implementation,
65+
"_multiarch",
66+
"gothic-iphoneos",
67+
raising=False,
68+
)
69+
70+
# Mock the return value of platform.ios_ver.
71+
def mock_ios_ver(*args):
72+
return ("iOS", "13.2", "iPhone15,2", False)
73+
74+
if sys.version_info < (3, 13):
75+
platform.ios_ver = mock_ios_ver
76+
else:
77+
monkeypatch.setattr(platform, "ios_ver", mock_ios_ver)
78+
79+
5680
class TestTag:
5781
def test_lowercasing(self):
5882
tag = tags.Tag("PY3", "None", "ANY")
@@ -335,6 +359,84 @@ def test_macos_11(self, major, minor):
335359
assert "macosx_12_0_universal2" in platforms
336360

337361

362+
class TestIOSPlatforms:
363+
def test_version_detection(self, mock_ios):
364+
platforms = list(tags.ios_platforms(multiarch="arm64-iphoneos"))
365+
assert platforms == [
366+
"ios_13_2_arm64_iphoneos",
367+
"ios_13_1_arm64_iphoneos",
368+
"ios_13_0_arm64_iphoneos",
369+
"ios_12_9_arm64_iphoneos",
370+
"ios_12_8_arm64_iphoneos",
371+
"ios_12_7_arm64_iphoneos",
372+
"ios_12_6_arm64_iphoneos",
373+
"ios_12_5_arm64_iphoneos",
374+
"ios_12_4_arm64_iphoneos",
375+
"ios_12_3_arm64_iphoneos",
376+
"ios_12_2_arm64_iphoneos",
377+
"ios_12_1_arm64_iphoneos",
378+
"ios_12_0_arm64_iphoneos",
379+
]
380+
381+
def test_multiarch_detection(self, mock_ios):
382+
platforms = list(tags.ios_platforms(version=(12, 0)))
383+
assert platforms == ["ios_12_0_gothic_iphoneos"]
384+
385+
def test_ios_platforms(self, mock_ios):
386+
# Pre-iOS 12.0 releases won't match anything
387+
platforms = list(tags.ios_platforms((7, 0), "arm64-iphoneos"))
388+
assert platforms == []
389+
390+
# iOS 12.0 returns exactly 1 match
391+
platforms = list(tags.ios_platforms((12, 0), "arm64-iphoneos"))
392+
assert platforms == ["ios_12_0_arm64_iphoneos"]
393+
394+
# iOS 13.0 returns a match for 13.0, plus every 12.X
395+
platforms = list(tags.ios_platforms((13, 0), "x86_64-iphonesimulator"))
396+
assert platforms == [
397+
"ios_13_0_x86_64_iphonesimulator",
398+
"ios_12_9_x86_64_iphonesimulator",
399+
"ios_12_8_x86_64_iphonesimulator",
400+
"ios_12_7_x86_64_iphonesimulator",
401+
"ios_12_6_x86_64_iphonesimulator",
402+
"ios_12_5_x86_64_iphonesimulator",
403+
"ios_12_4_x86_64_iphonesimulator",
404+
"ios_12_3_x86_64_iphonesimulator",
405+
"ios_12_2_x86_64_iphonesimulator",
406+
"ios_12_1_x86_64_iphonesimulator",
407+
"ios_12_0_x86_64_iphonesimulator",
408+
]
409+
410+
# iOS 14.3 returns a match for 14.3-14.0, plus every 13.X and every 12.X
411+
platforms = list(tags.ios_platforms((14, 3), "arm64-iphoneos"))
412+
assert platforms == [
413+
"ios_14_3_arm64_iphoneos",
414+
"ios_14_2_arm64_iphoneos",
415+
"ios_14_1_arm64_iphoneos",
416+
"ios_14_0_arm64_iphoneos",
417+
"ios_13_9_arm64_iphoneos",
418+
"ios_13_8_arm64_iphoneos",
419+
"ios_13_7_arm64_iphoneos",
420+
"ios_13_6_arm64_iphoneos",
421+
"ios_13_5_arm64_iphoneos",
422+
"ios_13_4_arm64_iphoneos",
423+
"ios_13_3_arm64_iphoneos",
424+
"ios_13_2_arm64_iphoneos",
425+
"ios_13_1_arm64_iphoneos",
426+
"ios_13_0_arm64_iphoneos",
427+
"ios_12_9_arm64_iphoneos",
428+
"ios_12_8_arm64_iphoneos",
429+
"ios_12_7_arm64_iphoneos",
430+
"ios_12_6_arm64_iphoneos",
431+
"ios_12_5_arm64_iphoneos",
432+
"ios_12_4_arm64_iphoneos",
433+
"ios_12_3_arm64_iphoneos",
434+
"ios_12_2_arm64_iphoneos",
435+
"ios_12_1_arm64_iphoneos",
436+
"ios_12_0_arm64_iphoneos",
437+
]
438+
439+
338440
class TestManylinuxPlatform:
339441
def teardown_method(self):
340442
# Clear the version cache
@@ -619,6 +721,7 @@ def test_linux_not_linux(self, monkeypatch):
619721
"platform_name,dispatch_func",
620722
[
621723
("Darwin", "mac_platforms"),
724+
("iOS", "ios_platforms"),
622725
("Linux", "_linux_platforms"),
623726
("Generic", "_generic_platforms"),
624727
],

0 commit comments

Comments
 (0)