diff --git a/docs/tags.rst b/docs/tags.rst index 26afe92fa..8ec8c559b 100644 --- a/docs/tags.rst +++ b/docs/tags.rst @@ -168,6 +168,21 @@ to the implementation to provide. Behavior of this method is undefined if invoked on non-iOS platforms without providing explicit version and multiarch arguments. + +.. function:: android_platforms(api_level=None, abi=None) + + Yields the :attr:`~Tag.platform` tags for Android. If this function is invoked on + non-Android platforms, the ``api_level`` and ``abi`` arguments are required. + + :param int api_level: The maximum `API level + `__ to return. Defaults + to the current system's version, as returned by ``platform.android_ver``. + :param str abi: The `Android ABI `__, + e.g. ``arm64_v8a``. Defaults to the current system's ABI , as returned by + ``sysconfig.get_platform``. Hyphens and periods will be replaced with + underscores. + + .. function:: platform_tags(version=None, arch=None) Yields the :attr:`~Tag.platform` tags for the running interpreter. diff --git a/src/packaging/tags.py b/src/packaging/tags.py index f5903402a..8522f59c4 100644 --- a/src/packaging/tags.py +++ b/src/packaging/tags.py @@ -530,6 +530,43 @@ def ios_platforms( ) +def android_platforms( + api_level: int | None = None, abi: str | None = None +) -> Iterator[str]: + """ + Yields the :attr:`~Tag.platform` tags for Android. If this function is invoked on + non-Android platforms, the ``api_level`` and ``abi`` arguments are required. + + :param int api_level: The maximum `API level + `__ to return. Defaults + to the current system's version, as returned by ``platform.android_ver``. + :param str abi: The `Android ABI `__, + e.g. ``arm64_v8a``. Defaults to the current system's ABI , as returned by + ``sysconfig.get_platform``. Hyphens and periods will be replaced with + underscores. + """ + if platform.system() != "Android" and (api_level is None or abi is None): + raise TypeError( + "on non-Android platforms, the api_level and abi arguments are required" + ) + + if api_level is None: + # Python 3.13 was the first version to return platform.system() == "Android", + # and also the first version to define platform.android_ver(). + api_level = platform.android_ver().api_level # type: ignore[attr-defined] + + if abi is None: + abi = sysconfig.get_platform().split("-")[-1] + abi = _normalize_string(abi) + + # 16 is the minimum API level known to have enough features to support CPython + # without major patching. Yield every API level from the maximum down to the + # minimum, inclusive. + min_api_level = 16 + for ver in range(api_level, min_api_level - 1, -1): + yield f"android_{ver}_{abi}" + + def _linux_platforms(is_32bit: bool = _32_BIT_INTERPRETER) -> Iterator[str]: linux = _normalize_string(sysconfig.get_platform()) if not linux.startswith("linux_"): @@ -561,6 +598,8 @@ def platform_tags() -> Iterator[str]: return mac_platforms() elif platform.system() == "iOS": return ios_platforms() + elif platform.system() == "Android": + return android_platforms() elif platform.system() == "Linux": return _linux_platforms() else: diff --git a/tests/test_tags.py b/tests/test_tags.py index 5ec89b244..012acb130 100644 --- a/tests/test_tags.py +++ b/tests/test_tags.py @@ -77,6 +77,23 @@ def mock_ios_ver(*args): monkeypatch.setattr(platform, "ios_ver", mock_ios_ver) +@pytest.fixture +def mock_android(monkeypatch): + monkeypatch.setattr(sys, "platform", "android") + monkeypatch.setattr(platform, "system", lambda: "Android") + monkeypatch.setattr(sysconfig, "get_platform", lambda: "android-21-arm64_v8a") + + AndroidVer = collections.namedtuple( + "AndroidVer", "release api_level manufacturer model device is_emulator" + ) + monkeypatch.setattr( + platform, + "android_ver", + lambda: AndroidVer("5.0", 21, "Google", "sdk_gphone64_arm64", "emu64a", True), + raising=False, # This function was added in Python 3.13. + ) + + class TestTag: def test_lowercasing(self): tag = tags.Tag("PY3", "None", "ANY") @@ -437,6 +454,69 @@ def test_ios_platforms(self, mock_ios): ] +class TestAndroidPlatforms: + def test_non_android(self): + non_android_error = pytest.raises(TypeError) + with non_android_error: + list(tags.android_platforms()) + with non_android_error: + list(tags.android_platforms(api_level=18)) + with non_android_error: + list(tags.android_platforms(abi="x86_64")) + + # The function can only be called on non-Android platforms if both arguments are + # provided. + assert list(tags.android_platforms(api_level=18, abi="x86_64")) == [ + "android_18_x86_64", + "android_17_x86_64", + "android_16_x86_64", + ] + + def test_detection(self, mock_android): + assert list(tags.android_platforms()) == [ + "android_21_arm64_v8a", + "android_20_arm64_v8a", + "android_19_arm64_v8a", + "android_18_arm64_v8a", + "android_17_arm64_v8a", + "android_16_arm64_v8a", + ] + + def test_api_level(self): + # API levels below the minimum should return nothing. + assert list(tags.android_platforms(api_level=14, abi="x86")) == [] + assert list(tags.android_platforms(api_level=15, abi="x86")) == [] + + assert list(tags.android_platforms(api_level=16, abi="x86")) == [ + "android_16_x86", + ] + assert list(tags.android_platforms(api_level=17, abi="x86")) == [ + "android_17_x86", + "android_16_x86", + ] + assert list(tags.android_platforms(api_level=18, abi="x86")) == [ + "android_18_x86", + "android_17_x86", + "android_16_x86", + ] + + def test_abi(self): + # Real ABI, normalized. + assert list(tags.android_platforms(api_level=16, abi="armeabi_v7a")) == [ + "android_16_armeabi_v7a", + ] + + # Real ABI, not normalized. + assert list(tags.android_platforms(api_level=16, abi="armeabi-v7a")) == [ + "android_16_armeabi_v7a", + ] + + # Nonexistent ABIs should still be accepted and normalized. + assert list(tags.android_platforms(api_level=16, abi="myarch-4.2")) == [ + "android_16_myarch_4_2", + ] + + class TestManylinuxPlatform: def teardown_method(self): # Clear the version cache @@ -722,6 +802,7 @@ def test_linux_not_linux(self, monkeypatch): [ ("Darwin", "mac_platforms"), ("iOS", "ios_platforms"), + ("Android", "android_platforms"), ("Linux", "_linux_platforms"), ("Generic", "_generic_platforms"), ],