Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-71042: Add platform.android_ver #116674

Merged
merged 9 commits into from
Mar 27, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
43 changes: 43 additions & 0 deletions Doc/library/platform.rst
Original file line number Diff line number Diff line change
Expand Up @@ -142,12 +142,19 @@ Cross Platform
Returns the system's release, e.g. ``'2.2.0'`` or ``'NT'``. An empty string is
returned if the value cannot be determined.

.. versionchanged:: 3.13
On Android, this now returns the Android version rather than the Linux
kernel version.


.. function:: system()

Returns the system/OS name, such as ``'Linux'``, ``'Darwin'``, ``'Java'``,
``'Windows'``. An empty string is returned if the value cannot be determined.

.. versionchanged:: 3.13
On Android, this now returns ``'Android'`` rather than ``'Linux'``.


.. function:: system_alias(system, release, version)

Expand Down Expand Up @@ -301,3 +308,39 @@ Linux Platforms
return ids

.. versionadded:: 3.10


Android Platform
----------------

.. function:: android_ver(release="", api_level=0, min_api_level=0, \
manufacturer="", model="", device="")

Get Android device information. Returns a :func:`~collections.namedtuple`
with the following attributes. Values which cannot be determined are set to
the defaults given as parameters.

* ``release`` - Android version, as a string (e.g. ``"14"``)

* ``api_level`` - API level, as an integer (e.g. ``34``)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you mind to repeat here the difference with https://docs.python.org/dev/library/sys.html#sys.getandroidapilevel and so add a reference to the sys function?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea; done.


* ``min_api_level`` - Minimum API level this build of Python can run on, as
an integer (e.g. ``21``)

* ``manufacturer`` - `manufacturer name
mhsmith marked this conversation as resolved.
Show resolved Hide resolved
<https://developer.android.com/reference/android/os/Build#MANUFACTURER>`__
(e.g. ``"Google"``)

* ``model`` - `model name
mhsmith marked this conversation as resolved.
Show resolved Hide resolved
<https://developer.android.com/reference/android/os/Build#MODEL>`__
(e.g. ``"Pixel 7"``)

* ``device`` - `device name
mhsmith marked this conversation as resolved.
Show resolved Hide resolved
<https://developer.android.com/reference/android/os/Build#DEVICE>`__
(e.g. ``"panther"``)

Which one of ``model`` and ``device`` is more likely to distinguish different
willingc marked this conversation as resolved.
Show resolved Hide resolved
device variants, and which one is more likely to resemble the marketing name,
varies between different manufacturers.

.. versionadded:: 3.13
4 changes: 3 additions & 1 deletion Doc/library/sys.rst
Original file line number Diff line number Diff line change
Expand Up @@ -753,7 +753,9 @@ always available.

.. function:: getandroidapilevel()

Return the build time API version of Android as an integer.
Return the build-time API level of Android as an integer. This represents the
minimum version of Android this build of Python can run on. For runtime
version information, see :func:`platform.android_ver`.

.. availability:: Android.

Expand Down
44 changes: 44 additions & 0 deletions Lib/platform.py
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,45 @@ def java_ver(release='', vendor='', vminfo=('', '', ''), osinfo=('', '', '')):

return release, vendor, vminfo, osinfo


AndroidVer = collections.namedtuple(
"AndroidVer",
"release api_level min_api_level manufacturer model device")

def android_ver(release="", api_level=0, min_api_level=0,
manufacturer="", model="", device=""):
if sys.platform == "android":
min_api_level = sys.getandroidapilevel()
try:
from ctypes import CDLL, c_char_p, create_string_buffer
mhsmith marked this conversation as resolved.
Show resolved Hide resolved
except ImportError:
pass
else:
# An NDK developer confirmed that this is an officially-supported
# API (https://stackoverflow.com/a/28416743). Use `getattr` to avoid
# private name mangling.
system_property_get = getattr(CDLL("libc.so"), "__system_property_get")
system_property_get.argtypes = (c_char_p, c_char_p)

def getprop(name, default):
PROP_VALUE_MAX = 92 # From sys/system_properties.h
mhsmith marked this conversation as resolved.
Show resolved Hide resolved
buffer = create_string_buffer(PROP_VALUE_MAX)
length = system_property_get(name.encode("UTF-8"), buffer)
if length == 0:
return default
else:
return buffer.value.decode("UTF-8", "backslashreplace")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you consider "surrogateescape" to not lose information? The caller can then decide how to handle surrogate characters. Did you see strings which cannot be decoded from UTF-8 in practice?

Copy link
Member Author

@mhsmith mhsmith Mar 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't seen such strings, but this function is likely to be used when building error reports, so perfect round-tripping is less important than making sure it doesn't cause any errors itself.

For that reason, I'd prefer not to use surrogateescape, because surrogates can't be turned back into bytes unless the encode call uses surrogateescape as well, which is something we have no control over, and something the programmer is very unlikely to test.


release = getprop("ro.build.version.release", release)
api_level = int(getprop("ro.build.version.sdk", api_level))
manufacturer = getprop("ro.product.manufacturer", manufacturer)
model = getprop("ro.product.model", model)
device = getprop("ro.product.device", device)

return AndroidVer(
release, api_level, min_api_level, manufacturer, model, device)


### System name aliasing

def system_alias(system, release, version):
Expand Down Expand Up @@ -972,6 +1011,11 @@ def uname():
system = 'Windows'
release = 'Vista'

# On Android, return the name and version of the OS rather than the kernel.
if sys.platform == 'android':
system = 'Android'
release = android_ver().release

vals = system, node, release, version, machine
# Replace 'unknown' values with the more portable ''
_uname_cache = uname_result(*map(_unknown_as_blank, vals))
Expand Down
3 changes: 3 additions & 0 deletions Lib/test/pythoninfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,9 @@ def collect_platform(info_add):
info_add(f'platform.freedesktop_os_release[{key}]',
os_release[key])

if sys.platform == 'android':
info_add('platform.android_ver', platform.android_ver())
mhsmith marked this conversation as resolved.
Show resolved Hide resolved


def collect_locale(info_add):
import locale
Expand Down
5 changes: 5 additions & 0 deletions Lib/test/test_asyncio/test_base_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import concurrent.futures
import errno
import math
import platform
import socket
import sys
import threading
Expand Down Expand Up @@ -1430,6 +1431,10 @@ def test_create_connection_no_inet_pton(self, m_socket):
self._test_create_connection_ip_addr(m_socket, False)

@patch_socket
@unittest.skipIf(
support.is_android and platform.android_ver().api_level < 23,
"Issue #26936: this fails on Android before API level 23"
mhsmith marked this conversation as resolved.
Show resolved Hide resolved
)
def test_create_connection_service_name(self, m_socket):
m_socket.getaddrinfo = socket.getaddrinfo
sock = m_socket.socket.return_value
Expand Down
54 changes: 54 additions & 0 deletions Lib/test/test_platform.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,19 @@ def test_uname(self):
self.assertEqual(res[-1], res.processor)
self.assertEqual(len(res), 6)

if os.name == "posix":
uname = os.uname()
self.assertEqual(res.node, uname.nodename)
self.assertEqual(res.version, uname.version)
self.assertEqual(res.machine, uname.machine)
mhsmith marked this conversation as resolved.
Show resolved Hide resolved

if sys.platform == "android":
self.assertEqual(res.system, "Android")
self.assertEqual(res.release, platform.android_ver().release)
else:
self.assertEqual(res.system, uname.sysname)
self.assertEqual(res.release, uname.release)

@unittest.skipUnless(sys.platform.startswith('win'), "windows only test")
def test_uname_win32_without_wmi(self):
def raises_oserror(*a):
Expand Down Expand Up @@ -430,6 +443,47 @@ def test_libc_ver(self):
self.assertEqual(platform.libc_ver(filename, chunksize=chunksize),
('glibc', '1.23.4'))

def test_android_ver(self):
res = platform.android_ver()
self.assertIsInstance(res, tuple)
self.assertEqual(res, (res.release, res.api_level, res.min_api_level,
res.manufacturer, res.model, res.device))

if sys.platform == "android":
for name in ["release", "manufacturer", "model", "device"]:
with self.subTest(name):
value = getattr(res, name)
self.assertIsInstance(value, str)
self.assertGreater(len(value), 0)
mhsmith marked this conversation as resolved.
Show resolved Hide resolved

for name in ["api_level", "min_api_level"]:
with self.subTest(name):
value = getattr(res, name)
self.assertIsInstance(value, int)
self.assertGreater(value, 0)

self.assertEqual(res.min_api_level, sys.getandroidapilevel())
self.assertGreaterEqual(res.api_level, res.min_api_level)

# When not running on Android, it should return the default values.
else:
self.assertEqual(res.release, "")
self.assertEqual(res.api_level, 0)
self.assertEqual(res.min_api_level, 0)
self.assertEqual(res.manufacturer, "")
self.assertEqual(res.model, "")
self.assertEqual(res.device, "")

# Default values may also be overridden using parameters.
res = platform.android_ver("alpha", 1, 2, "bravo", "charlie", "delta")
self.assertEqual(res.release, "alpha")
self.assertEqual(res.api_level, 1)
self.assertEqual(res.min_api_level, 2)
self.assertEqual(res.manufacturer, "bravo")
self.assertEqual(res.model, "charlie")
self.assertEqual(res.device, "delta")


@support.cpython_only
def test__comparable_version(self):
from platform import _comparable_version as V
Expand Down
18 changes: 11 additions & 7 deletions Lib/test/test_socket.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,10 @@ def socket_setdefaulttimeout(timeout):

HAVE_SOCKET_VSOCK = _have_socket_vsock()

HAVE_SOCKET_UDPLITE = hasattr(socket, "IPPROTO_UDPLITE")
# Older Android versions block UDPLITE with SELinux.
HAVE_SOCKET_UDPLITE = (
hasattr(socket, "IPPROTO_UDPLITE")
and not (support.is_android and platform.android_ver().api_level < 29))

HAVE_SOCKET_BLUETOOTH = _have_socket_bluetooth()

Expand Down Expand Up @@ -1217,8 +1220,8 @@ def testGetServBy(self):
else:
raise OSError
# Try same call with optional protocol omitted
# Issue #26936: Android getservbyname() was broken before API 23.
if (not support.is_android) or sys.getandroidapilevel() >= 23:
# Issue #26936: this fails on Android before API level 23.
mhsmith marked this conversation as resolved.
Show resolved Hide resolved
if not (support.is_android and platform.android_ver().api_level < 23):
port2 = socket.getservbyname(service)
eq(port, port2)
# Try udp, but don't barf if it doesn't exist
Expand All @@ -1229,8 +1232,9 @@ def testGetServBy(self):
else:
eq(udpport, port)
# Now make sure the lookup by port returns the same service name
# Issue #26936: Android getservbyport() is broken.
if not support.is_android:
# Issue #26936: when the protocol is omitted, this fails on Android
# before API level 28.
if not (support.is_android and platform.android_ver().api_level < 28):
eq(socket.getservbyport(port2), service)
eq(socket.getservbyport(port, 'tcp'), service)
if udpport is not None:
Expand Down Expand Up @@ -1575,8 +1579,8 @@ def testGetaddrinfo(self):
socket.getaddrinfo('::1', 80)
# port can be a string service name such as "http", a numeric
# port number or None
# Issue #26936: Android getaddrinfo() was broken before API level 23.
if (not support.is_android) or sys.getandroidapilevel() >= 23:
# Issue #26936: this fails on Android before API level 23.
if not (support.is_android and platform.android_ver().api_level < 23):
socket.getaddrinfo(HOST, "http")
socket.getaddrinfo(HOST, 80)
socket.getaddrinfo(HOST, None)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add :func:`platform.android_ver`, which provides device and OS information
on Android.
Loading