Skip to content

Commit

Permalink
gh-114099: Additions to standard library to support iOS (GH-117052)
Browse files Browse the repository at this point in the history
Co-authored-by: Hugo van Kemenade <[email protected]>
Co-authored-by: Malcolm Smith <[email protected]>
Co-authored-by: Ned Deily <[email protected]>
  • Loading branch information
4 people authored Mar 28, 2024
1 parent b448982 commit f006338
Show file tree
Hide file tree
Showing 22 changed files with 474 additions and 48 deletions.
5 changes: 5 additions & 0 deletions Doc/library/os.rst
Original file line number Diff line number Diff line change
Expand Up @@ -784,6 +784,11 @@ process and user.
:func:`socket.gethostname` or even
``socket.gethostbyaddr(socket.gethostname())``.

On macOS, iOS and Android, this returns the *kernel* name and version (i.e.,
``'Darwin'`` on macOS and iOS; ``'Linux'`` on Android). :func:`platform.uname()`
can be used to get the user-facing operating system name and version on iOS and
Android.

.. availability:: Unix.

.. versionchanged:: 3.3
Expand Down
24 changes: 23 additions & 1 deletion Doc/library/platform.rst
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,9 @@ Cross Platform
Returns the system/OS name, such as ``'Linux'``, ``'Darwin'``, ``'Java'``,
``'Windows'``. An empty string is returned if the value cannot be determined.

On iOS and Android, this returns the user-facing OS name (i.e, ``'iOS``,
``'iPadOS'`` or ``'Android'``). To obtain the kernel name (``'Darwin'`` or
``'Linux'``), use :func:`os.uname()`.

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

Expand All @@ -161,6 +164,8 @@ Cross Platform
Returns the system's release version, e.g. ``'#3 on degas'``. An empty string is
returned if the value cannot be determined.

On iOS and Android, this is the user-facing OS version. To obtain the
Darwin or Linux kernel version, use :func:`os.uname()`.

.. function:: uname()

Expand Down Expand Up @@ -238,7 +243,6 @@ Windows Platform
macOS Platform
--------------


.. function:: mac_ver(release='', versioninfo=('','',''), machine='')

Get macOS version information and return it as tuple ``(release, versioninfo,
Expand All @@ -248,6 +252,24 @@ macOS Platform
Entries which cannot be determined are set to ``''``. All tuple entries are
strings.

iOS Platform
------------

.. function:: ios_ver(system='', release='', model='', is_simulator=False)

Get iOS version information and return it as a
:func:`~collections.namedtuple` with the following attributes:

* ``system`` is the OS name; either ``'iOS'`` or ``'iPadOS'``.
* ``release`` is the iOS version number as a string (e.g., ``'17.2'``).
* ``model`` is the device model identifier; this will be a string like
``'iPhone13,2'`` for a physical device, or ``'iPhone'`` on a simulator.
* ``is_simulator`` is a boolean describing if the app is running on a
simulator or a physical device.

Entries which cannot be determined are set to the defaults given as
parameters.


Unix Platforms
--------------
Expand Down
17 changes: 16 additions & 1 deletion Doc/library/webbrowser.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ allow the remote browser to maintain its own windows on the display. If remote
browsers are not available on Unix, the controlling process will launch a new
browser and wait.

On iOS, the :envvar:`BROWSER` environment variable, as well as any arguments
controlling autoraise, browser preference, and new tab/window creation will be
ignored. Web pages will *always* be opened in the user's preferred browser, in
a new tab, with the browser being brought to the foreground. The use of the
:mod:`webbrowser` module on iOS requires the :mod:`ctypes` module. If
:mod:`ctypes` isn't available, calls to :func:`.open` will fail.

The script :program:`webbrowser` can be used as a command-line interface for the
module. It accepts a URL as the argument. It accepts the following optional
parameters: ``-n`` opens the URL in a new browser window, if possible;
Expand Down Expand Up @@ -147,6 +154,8 @@ for the controller classes, all defined in this module.
+------------------------+-----------------------------------------+-------+
| ``'chromium-browser'`` | ``Chromium('chromium-browser')`` | |
+------------------------+-----------------------------------------+-------+
| ``'iosbrowser'`` | ``IOSBrowser`` | \(4) |
+------------------------+-----------------------------------------+-------+

Notes:

Expand All @@ -161,7 +170,10 @@ Notes:
Only on Windows platforms.

(3)
Only on macOS platform.
Only on macOS.

(4)
Only on iOS.

.. versionadded:: 3.2
A new :class:`!MacOSXOSAScript` class has been added
Expand All @@ -176,6 +188,9 @@ Notes:
Removed browsers include Grail, Mosaic, Netscape, Galeon,
Skipstone, Iceape, and Firefox versions 35 and below.

.. versionchanged:: 3.13
Support for iOS has been added.

Here are some simple examples::

url = 'https://docs.python.org/'
Expand Down
71 changes: 71 additions & 0 deletions Lib/_ios_support.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import sys
try:
from ctypes import cdll, c_void_p, c_char_p, util
except ImportError:
# ctypes is an optional module. If it's not present, we're limited in what
# we can tell about the system, but we don't want to prevent the module
# from working.
print("ctypes isn't available; iOS system calls will not be available")
objc = None
else:
# ctypes is available. Load the ObjC library, and wrap the objc_getClass,
# sel_registerName methods
lib = util.find_library("objc")
if lib is None:
# Failed to load the objc library
raise RuntimeError("ObjC runtime library couldn't be loaded")

objc = cdll.LoadLibrary(lib)
objc.objc_getClass.restype = c_void_p
objc.objc_getClass.argtypes = [c_char_p]
objc.sel_registerName.restype = c_void_p
objc.sel_registerName.argtypes = [c_char_p]


def get_platform_ios():
# Determine if this is a simulator using the multiarch value
is_simulator = sys.implementation._multiarch.endswith("simulator")

# We can't use ctypes; abort
if not objc:
return None

# Most of the methods return ObjC objects
objc.objc_msgSend.restype = c_void_p
# All the methods used have no arguments.
objc.objc_msgSend.argtypes = [c_void_p, c_void_p]

# Equivalent of:
# device = [UIDevice currentDevice]
UIDevice = objc.objc_getClass(b"UIDevice")
SEL_currentDevice = objc.sel_registerName(b"currentDevice")
device = objc.objc_msgSend(UIDevice, SEL_currentDevice)

# Equivalent of:
# device_systemVersion = [device systemVersion]
SEL_systemVersion = objc.sel_registerName(b"systemVersion")
device_systemVersion = objc.objc_msgSend(device, SEL_systemVersion)

# Equivalent of:
# device_systemName = [device systemName]
SEL_systemName = objc.sel_registerName(b"systemName")
device_systemName = objc.objc_msgSend(device, SEL_systemName)

# Equivalent of:
# device_model = [device model]
SEL_model = objc.sel_registerName(b"model")
device_model = objc.objc_msgSend(device, SEL_model)

# UTF8String returns a const char*;
SEL_UTF8String = objc.sel_registerName(b"UTF8String")
objc.objc_msgSend.restype = c_char_p

# Equivalent of:
# system = [device_systemName UTF8String]
# release = [device_systemVersion UTF8String]
# model = [device_model UTF8String]
system = objc.objc_msgSend(device_systemName, SEL_UTF8String).decode()
release = objc.objc_msgSend(device_systemVersion, SEL_UTF8String).decode()
model = objc.objc_msgSend(device_model, SEL_UTF8String).decode()

return system, release, model, is_simulator
53 changes: 46 additions & 7 deletions Lib/platform.py
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,30 @@ def mac_ver(release='', versioninfo=('', '', ''), machine=''):
# If that also doesn't work return the default values
return release, versioninfo, machine


# A namedtuple for iOS version information.
IOSVersionInfo = collections.namedtuple(
"IOSVersionInfo",
["system", "release", "model", "is_simulator"]
)


def ios_ver(system="", release="", model="", is_simulator=False):
"""Get iOS version information, and return it as a namedtuple:
(system, release, model, is_simulator).
If values can't be determined, they are set to values provided as
parameters.
"""
if sys.platform == "ios":
import _ios_support
result = _ios_support.get_platform_ios()
if result is not None:
return IOSVersionInfo(*result)

return IOSVersionInfo(system, release, model, is_simulator)


def _java_getprop(name, default):
"""This private helper is deprecated in 3.13 and will be removed in 3.15"""
from java.lang import System
Expand Down Expand Up @@ -654,7 +678,7 @@ def _platform(*args):
if cleaned == platform:
break
platform = cleaned
while platform[-1] == '-':
while platform and platform[-1] == '-':
platform = platform[:-1]

return platform
Expand Down Expand Up @@ -695,7 +719,7 @@ def _syscmd_file(target, default=''):
default in case the command should fail.
"""
if sys.platform in ('dos', 'win32', 'win16'):
if sys.platform in {'dos', 'win32', 'win16', 'ios', 'tvos', 'watchos'}:
# XXX Others too ?
return default

Expand Down Expand Up @@ -859,6 +883,14 @@ def get_OpenVMS():
csid, cpu_number = vms_lib.getsyi('SYI$_CPU', 0)
return 'Alpha' if cpu_number >= 128 else 'VAX'

# On the iOS simulator, os.uname returns the architecture as uname.machine.
# On device it returns the model name for some reason; but there's only one
# CPU architecture for iOS devices, so we know the right answer.
def get_ios():
if sys.implementation._multiarch.endswith("simulator"):
return os.uname().machine
return 'arm64'

def from_subprocess():
"""
Fall back to `uname -p`
Expand Down Expand Up @@ -1018,6 +1050,10 @@ def uname():
system = 'Android'
release = android_ver().release

# Normalize responses on iOS
if sys.platform == 'ios':
system, release, _, _ = ios_ver()

vals = system, node, release, version, machine
# Replace 'unknown' values with the more portable ''
_uname_cache = uname_result(*map(_unknown_as_blank, vals))
Expand Down Expand Up @@ -1297,11 +1333,14 @@ def platform(aliased=False, terse=False):
system, release, version = system_alias(system, release, version)

if system == 'Darwin':
# macOS (darwin kernel)
macos_release = mac_ver()[0]
if macos_release:
system = 'macOS'
release = macos_release
# macOS and iOS both report as a "Darwin" kernel
if sys.platform == "ios":
system, release, _, _ = ios_ver()
else:
macos_release = mac_ver()[0]
if macos_release:
system = 'macOS'
release = macos_release

if system == 'Windows':
# MS platforms
Expand Down
4 changes: 2 additions & 2 deletions Lib/site.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,8 +280,8 @@ def _getuserbase():
if env_base:
return env_base

# Emscripten, VxWorks, and WASI have no home directories
if sys.platform in {"emscripten", "vxworks", "wasi"}:
# Emscripten, iOS, tvOS, VxWorks, WASI, and watchOS have no home directories
if sys.platform in {"emscripten", "ios", "tvos", "vxworks", "wasi", "watchos"}:
return None

def joinuser(*args):
Expand Down
20 changes: 14 additions & 6 deletions Lib/sysconfig/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

# Keys for get_config_var() that are never converted to Python integers.
_ALWAYS_STR = {
'IPHONEOS_DEPLOYMENT_TARGET',
'MACOSX_DEPLOYMENT_TARGET',
}

Expand Down Expand Up @@ -57,6 +58,7 @@
'scripts': '{base}/Scripts',
'data': '{base}',
},

# Downstream distributors can overwrite the default install scheme.
# This is done to support downstream modifications where distributors change
# the installation layout (eg. different site-packages directory).
Expand Down Expand Up @@ -114,8 +116,8 @@ def _getuserbase():
if env_base:
return env_base

# Emscripten, VxWorks, and WASI have no home directories
if sys.platform in {"emscripten", "vxworks", "wasi"}:
# Emscripten, iOS, tvOS, VxWorks, WASI, and watchOS have no home directories
if sys.platform in {"emscripten", "ios", "tvos", "vxworks", "wasi", "watchos"}:
return None

def joinuser(*args):
Expand Down Expand Up @@ -290,6 +292,7 @@ def _get_preferred_schemes():
'home': 'posix_home',
'user': 'osx_framework_user',
}

return {
'prefix': 'posix_prefix',
'home': 'posix_home',
Expand Down Expand Up @@ -623,10 +626,15 @@ def get_platform():
if m:
release = m.group()
elif osname[:6] == "darwin":
import _osx_support
osname, release, machine = _osx_support.get_platform_osx(
get_config_vars(),
osname, release, machine)
if sys.platform == "ios":
release = get_config_vars().get("IPHONEOS_DEPLOYMENT_TARGET", "12.0")
osname = sys.platform
machine = sys.implementation._multiarch
else:
import _osx_support
osname, release, machine = _osx_support.get_platform_osx(
get_config_vars(),
osname, release, machine)

return f"{osname}-{release}-{machine}"

Expand Down
1 change: 1 addition & 0 deletions Lib/test/pythoninfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ def format_groups(groups):
"HOMEDRIVE",
"HOMEPATH",
"IDLESTARTUP",
"IPHONEOS_DEPLOYMENT_TARGET",
"LANG",
"LDFLAGS",
"LDSHARED",
Expand Down
1 change: 1 addition & 0 deletions Lib/test/test_concurrent_futures/test_thread_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ def test_idle_thread_reuse(self):
self.assertEqual(len(executor._threads), 1)
executor.shutdown(wait=True)

@support.requires_fork()
@unittest.skipUnless(hasattr(os, 'register_at_fork'), 'need os.register_at_fork')
@support.requires_resource('cpu')
def test_hang_global_shutdown_lock(self):
Expand Down
1 change: 1 addition & 0 deletions Lib/test/test_gc.py
Original file line number Diff line number Diff line change
Expand Up @@ -1223,6 +1223,7 @@ def test_collect_garbage(self):
self.assertEqual(len(gc.garbage), 0)


@requires_subprocess()
@unittest.skipIf(BUILD_WITH_NDEBUG,
'built with -NDEBUG')
def test_refcount_errors(self):
Expand Down
Loading

0 comments on commit f006338

Please sign in to comment.