Skip to content

Commit 8e49b43

Browse files
mhsmithbrettcannon
andauthored
Add support for PEP 738 Android tags (#880)
Co-authored-by: Brett Cannon <[email protected]>
1 parent e624d8e commit 8e49b43

File tree

3 files changed

+135
-0
lines changed

3 files changed

+135
-0
lines changed

docs/tags.rst

+15
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,21 @@ to the implementation to provide.
168168
Behavior of this method is undefined if invoked on non-iOS platforms
169169
without providing explicit version and multiarch arguments.
170170

171+
172+
.. function:: android_platforms(api_level=None, abi=None)
173+
174+
Yields the :attr:`~Tag.platform` tags for Android. If this function is invoked on
175+
non-Android platforms, the ``api_level`` and ``abi`` arguments are required.
176+
177+
:param int api_level: The maximum `API level
178+
<https://developer.android.com/tools/releases/platforms>`__ to return. Defaults
179+
to the current system's version, as returned by ``platform.android_ver``.
180+
:param str abi: The `Android ABI <https://developer.android.com/ndk/guides/abis>`__,
181+
e.g. ``arm64_v8a``. Defaults to the current system's ABI , as returned by
182+
``sysconfig.get_platform``. Hyphens and periods will be replaced with
183+
underscores.
184+
185+
171186
.. function:: platform_tags(version=None, arch=None)
172187

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

src/packaging/tags.py

+39
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,43 @@ def ios_platforms(
530530
)
531531

532532

533+
def android_platforms(
534+
api_level: int | None = None, abi: str | None = None
535+
) -> Iterator[str]:
536+
"""
537+
Yields the :attr:`~Tag.platform` tags for Android. If this function is invoked on
538+
non-Android platforms, the ``api_level`` and ``abi`` arguments are required.
539+
540+
:param int api_level: The maximum `API level
541+
<https://developer.android.com/tools/releases/platforms>`__ to return. Defaults
542+
to the current system's version, as returned by ``platform.android_ver``.
543+
:param str abi: The `Android ABI <https://developer.android.com/ndk/guides/abis>`__,
544+
e.g. ``arm64_v8a``. Defaults to the current system's ABI , as returned by
545+
``sysconfig.get_platform``. Hyphens and periods will be replaced with
546+
underscores.
547+
"""
548+
if platform.system() != "Android" and (api_level is None or abi is None):
549+
raise TypeError(
550+
"on non-Android platforms, the api_level and abi arguments are required"
551+
)
552+
553+
if api_level is None:
554+
# Python 3.13 was the first version to return platform.system() == "Android",
555+
# and also the first version to define platform.android_ver().
556+
api_level = platform.android_ver().api_level # type: ignore[attr-defined]
557+
558+
if abi is None:
559+
abi = sysconfig.get_platform().split("-")[-1]
560+
abi = _normalize_string(abi)
561+
562+
# 16 is the minimum API level known to have enough features to support CPython
563+
# without major patching. Yield every API level from the maximum down to the
564+
# minimum, inclusive.
565+
min_api_level = 16
566+
for ver in range(api_level, min_api_level - 1, -1):
567+
yield f"android_{ver}_{abi}"
568+
569+
533570
def _linux_platforms(is_32bit: bool = _32_BIT_INTERPRETER) -> Iterator[str]:
534571
linux = _normalize_string(sysconfig.get_platform())
535572
if not linux.startswith("linux_"):
@@ -561,6 +598,8 @@ def platform_tags() -> Iterator[str]:
561598
return mac_platforms()
562599
elif platform.system() == "iOS":
563600
return ios_platforms()
601+
elif platform.system() == "Android":
602+
return android_platforms()
564603
elif platform.system() == "Linux":
565604
return _linux_platforms()
566605
else:

tests/test_tags.py

+81
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,23 @@ def mock_ios_ver(*args):
7777
monkeypatch.setattr(platform, "ios_ver", mock_ios_ver)
7878

7979

80+
@pytest.fixture
81+
def mock_android(monkeypatch):
82+
monkeypatch.setattr(sys, "platform", "android")
83+
monkeypatch.setattr(platform, "system", lambda: "Android")
84+
monkeypatch.setattr(sysconfig, "get_platform", lambda: "android-21-arm64_v8a")
85+
86+
AndroidVer = collections.namedtuple(
87+
"AndroidVer", "release api_level manufacturer model device is_emulator"
88+
)
89+
monkeypatch.setattr(
90+
platform,
91+
"android_ver",
92+
lambda: AndroidVer("5.0", 21, "Google", "sdk_gphone64_arm64", "emu64a", True),
93+
raising=False, # This function was added in Python 3.13.
94+
)
95+
96+
8097
class TestTag:
8198
def test_lowercasing(self):
8299
tag = tags.Tag("PY3", "None", "ANY")
@@ -437,6 +454,69 @@ def test_ios_platforms(self, mock_ios):
437454
]
438455

439456

457+
class TestAndroidPlatforms:
458+
def test_non_android(self):
459+
non_android_error = pytest.raises(TypeError)
460+
with non_android_error:
461+
list(tags.android_platforms())
462+
with non_android_error:
463+
list(tags.android_platforms(api_level=18))
464+
with non_android_error:
465+
list(tags.android_platforms(abi="x86_64"))
466+
467+
# The function can only be called on non-Android platforms if both arguments are
468+
# provided.
469+
assert list(tags.android_platforms(api_level=18, abi="x86_64")) == [
470+
"android_18_x86_64",
471+
"android_17_x86_64",
472+
"android_16_x86_64",
473+
]
474+
475+
def test_detection(self, mock_android):
476+
assert list(tags.android_platforms()) == [
477+
"android_21_arm64_v8a",
478+
"android_20_arm64_v8a",
479+
"android_19_arm64_v8a",
480+
"android_18_arm64_v8a",
481+
"android_17_arm64_v8a",
482+
"android_16_arm64_v8a",
483+
]
484+
485+
def test_api_level(self):
486+
# API levels below the minimum should return nothing.
487+
assert list(tags.android_platforms(api_level=14, abi="x86")) == []
488+
assert list(tags.android_platforms(api_level=15, abi="x86")) == []
489+
490+
assert list(tags.android_platforms(api_level=16, abi="x86")) == [
491+
"android_16_x86",
492+
]
493+
assert list(tags.android_platforms(api_level=17, abi="x86")) == [
494+
"android_17_x86",
495+
"android_16_x86",
496+
]
497+
assert list(tags.android_platforms(api_level=18, abi="x86")) == [
498+
"android_18_x86",
499+
"android_17_x86",
500+
"android_16_x86",
501+
]
502+
503+
def test_abi(self):
504+
# Real ABI, normalized.
505+
assert list(tags.android_platforms(api_level=16, abi="armeabi_v7a")) == [
506+
"android_16_armeabi_v7a",
507+
]
508+
509+
# Real ABI, not normalized.
510+
assert list(tags.android_platforms(api_level=16, abi="armeabi-v7a")) == [
511+
"android_16_armeabi_v7a",
512+
]
513+
514+
# Nonexistent ABIs should still be accepted and normalized.
515+
assert list(tags.android_platforms(api_level=16, abi="myarch-4.2")) == [
516+
"android_16_myarch_4_2",
517+
]
518+
519+
440520
class TestManylinuxPlatform:
441521
def teardown_method(self):
442522
# Clear the version cache
@@ -722,6 +802,7 @@ def test_linux_not_linux(self, monkeypatch):
722802
[
723803
("Darwin", "mac_platforms"),
724804
("iOS", "ios_platforms"),
805+
("Android", "android_platforms"),
725806
("Linux", "_linux_platforms"),
726807
("Generic", "_generic_platforms"),
727808
],

0 commit comments

Comments
 (0)