Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions docs/tags.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
<https://developer.android.com/tools/releases/platforms>`__ to return. Defaults
to the current system's version, as returned by ``platform.android_ver``.
:param str abi: The `Android ABI <https://developer.android.com/ndk/guides/abis>`__,
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.
Expand Down
39 changes: 39 additions & 0 deletions src/packaging/tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
<https://developer.android.com/tools/releases/platforms>`__ to return. Defaults
to the current system's version, as returned by ``platform.android_ver``.
:param str abi: The `Android ABI <https://developer.android.com/ndk/guides/abis>`__,
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_"):
Expand Down Expand Up @@ -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:
Expand Down
81 changes: 81 additions & 0 deletions tests/test_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"),
],
Expand Down
Loading