Skip to content

Commit ed25e8b

Browse files
freakboy3742hugovkmhsmithned-deily
committed
[3.12] pythongh-114099: Additions to standard library to support iOS (pythonGH-117052)
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Co-authored-by: Malcolm Smith <smith@chaquo.com> Co-authored-by: Ned Deily <nad@python.org>
1 parent 354655a commit ed25e8b

25 files changed

+489
-53
lines changed

Doc/library/os.rst

+5
Original file line numberDiff line numberDiff line change
@@ -784,6 +784,11 @@ process and user.
784784
:func:`socket.gethostname` or even
785785
``socket.gethostbyaddr(socket.gethostname())``.
786786

787+
On macOS, iOS and Android, this returns the *kernel* name and version (i.e.,
788+
``'Darwin'`` on macOS and iOS; ``'Linux'`` on Android). :func:`platform.uname()`
789+
can be used to get the user-facing operating system name and version on iOS and
790+
Android.
791+
787792
.. availability:: Unix.
788793

789794
.. versionchanged:: 3.3

Doc/library/platform.rst

+23-1
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,9 @@ Cross Platform
148148
Returns the system/OS name, such as ``'Linux'``, ``'Darwin'``, ``'Java'``,
149149
``'Windows'``. An empty string is returned if the value cannot be determined.
150150

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

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

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

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

165170
.. function:: uname()
166171

@@ -234,7 +239,6 @@ Windows Platform
234239
macOS Platform
235240
--------------
236241

237-
238242
.. function:: mac_ver(release='', versioninfo=('','',''), machine='')
239243

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

251+
iOS Platform
252+
------------
253+
254+
.. function:: ios_ver(system='', release='', model='', is_simulator=False)
255+
256+
Get iOS version information and return it as a
257+
:func:`~collections.namedtuple` with the following attributes:
258+
259+
* ``system`` is the OS name; either ``'iOS'`` or ``'iPadOS'``.
260+
* ``release`` is the iOS version number as a string (e.g., ``'17.2'``).
261+
* ``model`` is the device model identifier; this will be a string like
262+
``'iPhone13,2'`` for a physical device, or ``'iPhone'`` on a simulator.
263+
* ``is_simulator`` is a boolean describing if the app is running on a
264+
simulator or a physical device.
265+
266+
Entries which cannot be determined are set to the defaults given as
267+
parameters.
268+
247269

248270
Unix Platforms
249271
--------------

Doc/library/webbrowser.rst

+17-1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@ allow the remote browser to maintain its own windows on the display. If remote
3333
browsers are not available on Unix, the controlling process will launch a new
3434
browser and wait.
3535

36+
On iOS, the :envvar:`BROWSER` environment variable, as well as any arguments
37+
controlling autoraise, browser preference, and new tab/window creation will be
38+
ignored. Web pages will *always* be opened in the user's preferred browser, in
39+
a new tab, with the browser being brought to the foreground. The use of the
40+
:mod:`webbrowser` module on iOS requires the :mod:`ctypes` module. If
41+
:mod:`ctypes` isn't available, calls to :func:`.open` will fail.
42+
3643
The script :program:`webbrowser` can be used as a command-line interface for the
3744
module. It accepts a URL as the argument. It accepts the following optional
3845
parameters: ``-n`` opens the URL in a new browser window, if possible;
@@ -154,6 +161,8 @@ for the controller classes, all defined in this module.
154161
+------------------------+-----------------------------------------+-------+
155162
| ``'chromium-browser'`` | :class:`Chromium('chromium-browser')` | |
156163
+------------------------+-----------------------------------------+-------+
164+
| ``'iosbrowser'`` | ``IOSBrowser`` | \(4) |
165+
+------------------------+-----------------------------------------+-------+
157166

158167
Notes:
159168

@@ -168,7 +177,11 @@ Notes:
168177
Only on Windows platforms.
169178

170179
(3)
171-
Only on macOS platform.
180+
Only on macOS.
181+
182+
(4)
183+
Only on iOS.
184+
172185

173186
.. versionadded:: 3.3
174187
Support for Chrome/Chromium has been added.
@@ -181,6 +194,9 @@ Notes:
181194
.. deprecated-removed:: 3.11 3.13
182195
:class:`MacOSX` is deprecated, use :class:`MacOSXOSAScript` instead.
183196

197+
.. versionchanged:: 3.13
198+
Support for iOS has been added.
199+
184200
Here are some simple examples::
185201

186202
url = 'https://docs.python.org/'

Lib/_ios_support.py

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import sys
2+
try:
3+
from ctypes import cdll, c_void_p, c_char_p, util
4+
except ImportError:
5+
# ctypes is an optional module. If it's not present, we're limited in what
6+
# we can tell about the system, but we don't want to prevent the module
7+
# from working.
8+
print("ctypes isn't available; iOS system calls will not be available")
9+
objc = None
10+
else:
11+
# ctypes is available. Load the ObjC library, and wrap the objc_getClass,
12+
# sel_registerName methods
13+
lib = util.find_library("objc")
14+
if lib is None:
15+
# Failed to load the objc library
16+
raise RuntimeError("ObjC runtime library couldn't be loaded")
17+
18+
objc = cdll.LoadLibrary(lib)
19+
objc.objc_getClass.restype = c_void_p
20+
objc.objc_getClass.argtypes = [c_char_p]
21+
objc.sel_registerName.restype = c_void_p
22+
objc.sel_registerName.argtypes = [c_char_p]
23+
24+
25+
def get_platform_ios():
26+
# Determine if this is a simulator using the multiarch value
27+
is_simulator = sys.implementation._multiarch.endswith("simulator")
28+
29+
# We can't use ctypes; abort
30+
if not objc:
31+
return None
32+
33+
# Most of the methods return ObjC objects
34+
objc.objc_msgSend.restype = c_void_p
35+
# All the methods used have no arguments.
36+
objc.objc_msgSend.argtypes = [c_void_p, c_void_p]
37+
38+
# Equivalent of:
39+
# device = [UIDevice currentDevice]
40+
UIDevice = objc.objc_getClass(b"UIDevice")
41+
SEL_currentDevice = objc.sel_registerName(b"currentDevice")
42+
device = objc.objc_msgSend(UIDevice, SEL_currentDevice)
43+
44+
# Equivalent of:
45+
# device_systemVersion = [device systemVersion]
46+
SEL_systemVersion = objc.sel_registerName(b"systemVersion")
47+
device_systemVersion = objc.objc_msgSend(device, SEL_systemVersion)
48+
49+
# Equivalent of:
50+
# device_systemName = [device systemName]
51+
SEL_systemName = objc.sel_registerName(b"systemName")
52+
device_systemName = objc.objc_msgSend(device, SEL_systemName)
53+
54+
# Equivalent of:
55+
# device_model = [device model]
56+
SEL_model = objc.sel_registerName(b"model")
57+
device_model = objc.objc_msgSend(device, SEL_model)
58+
59+
# UTF8String returns a const char*;
60+
SEL_UTF8String = objc.sel_registerName(b"UTF8String")
61+
objc.objc_msgSend.restype = c_char_p
62+
63+
# Equivalent of:
64+
# system = [device_systemName UTF8String]
65+
# release = [device_systemVersion UTF8String]
66+
# model = [device_model UTF8String]
67+
system = objc.objc_msgSend(device_systemName, SEL_UTF8String).decode()
68+
release = objc.objc_msgSend(device_systemVersion, SEL_UTF8String).decode()
69+
model = objc.objc_msgSend(device_model, SEL_UTF8String).decode()
70+
71+
return system, release, model, is_simulator

Lib/platform.py

+46-7
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,30 @@ def mac_ver(release='', versioninfo=('', '', ''), machine=''):
497497
# If that also doesn't work return the default values
498498
return release, versioninfo, machine
499499

500+
501+
# A namedtuple for iOS version information.
502+
IOSVersionInfo = collections.namedtuple(
503+
"IOSVersionInfo",
504+
["system", "release", "model", "is_simulator"]
505+
)
506+
507+
508+
def ios_ver(system="", release="", model="", is_simulator=False):
509+
"""Get iOS version information, and return it as a namedtuple:
510+
(system, release, model, is_simulator).
511+
512+
If values can't be determined, they are set to values provided as
513+
parameters.
514+
"""
515+
if sys.platform == "ios":
516+
import _ios_support
517+
result = _ios_support.get_platform_ios()
518+
if result is not None:
519+
return IOSVersionInfo(*result)
520+
521+
return IOSVersionInfo(system, release, model, is_simulator)
522+
523+
500524
def _java_getprop(name, default):
501525

502526
from java.lang import System
@@ -612,7 +636,7 @@ def _platform(*args):
612636
if cleaned == platform:
613637
break
614638
platform = cleaned
615-
while platform[-1] == '-':
639+
while platform and platform[-1] == '-':
616640
platform = platform[:-1]
617641

618642
return platform
@@ -653,7 +677,7 @@ def _syscmd_file(target, default=''):
653677
default in case the command should fail.
654678
655679
"""
656-
if sys.platform in ('dos', 'win32', 'win16'):
680+
if sys.platform in {'dos', 'win32', 'win16', 'ios', 'tvos', 'watchos'}:
657681
# XXX Others too ?
658682
return default
659683

@@ -815,6 +839,14 @@ def get_OpenVMS():
815839
csid, cpu_number = vms_lib.getsyi('SYI$_CPU', 0)
816840
return 'Alpha' if cpu_number >= 128 else 'VAX'
817841

842+
# On the iOS simulator, os.uname returns the architecture as uname.machine.
843+
# On device it returns the model name for some reason; but there's only one
844+
# CPU architecture for iOS devices, so we know the right answer.
845+
def get_ios():
846+
if sys.implementation._multiarch.endswith("simulator"):
847+
return os.uname().machine
848+
return 'arm64'
849+
818850
def from_subprocess():
819851
"""
820852
Fall back to `uname -p`
@@ -969,6 +1001,10 @@ def uname():
9691001
system = 'Windows'
9701002
release = 'Vista'
9711003

1004+
# Normalize responses on iOS
1005+
if sys.platform == 'ios':
1006+
system, release, _, _ = ios_ver()
1007+
9721008
vals = system, node, release, version, machine
9731009
# Replace 'unknown' values with the more portable ''
9741010
_uname_cache = uname_result(*map(_unknown_as_blank, vals))
@@ -1248,11 +1284,14 @@ def platform(aliased=False, terse=False):
12481284
system, release, version = system_alias(system, release, version)
12491285

12501286
if system == 'Darwin':
1251-
# macOS (darwin kernel)
1252-
macos_release = mac_ver()[0]
1253-
if macos_release:
1254-
system = 'macOS'
1255-
release = macos_release
1287+
# macOS and iOS both report as a "Darwin" kernel
1288+
if sys.platform == "ios":
1289+
system, release, _, _ = ios_ver()
1290+
else:
1291+
macos_release = mac_ver()[0]
1292+
if macos_release:
1293+
system = 'macOS'
1294+
release = macos_release
12561295

12571296
if system == 'Windows':
12581297
# MS platforms

Lib/site.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -287,8 +287,8 @@ def _getuserbase():
287287
if env_base:
288288
return env_base
289289

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

294294
def joinuser(*args):

Lib/sysconfig.py

+14-6
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
# Keys for get_config_var() that are never converted to Python integers.
2323
_ALWAYS_STR = {
24+
'IPHONEOS_DEPLOYMENT_TARGET',
2425
'MACOSX_DEPLOYMENT_TARGET',
2526
}
2627

@@ -57,6 +58,7 @@
5758
'scripts': '{base}/Scripts',
5859
'data': '{base}',
5960
},
61+
6062
# Downstream distributors can overwrite the default install scheme.
6163
# This is done to support downstream modifications where distributors change
6264
# the installation layout (eg. different site-packages directory).
@@ -112,8 +114,8 @@ def _getuserbase():
112114
if env_base:
113115
return env_base
114116

115-
# Emscripten, VxWorks, and WASI have no home directories
116-
if sys.platform in {"emscripten", "vxworks", "wasi"}:
117+
# Emscripten, iOS, tvOS, VxWorks, WASI, and watchOS have no home directories
118+
if sys.platform in {"emscripten", "ios", "tvos", "vxworks", "wasi", "watchos"}:
117119
return None
118120

119121
def joinuser(*args):
@@ -294,6 +296,7 @@ def _get_preferred_schemes():
294296
'home': 'posix_home',
295297
'user': 'osx_framework_user',
296298
}
299+
297300
return {
298301
'prefix': 'posix_prefix',
299302
'home': 'posix_home',
@@ -813,10 +816,15 @@ def get_platform():
813816
if m:
814817
release = m.group()
815818
elif osname[:6] == "darwin":
816-
import _osx_support
817-
osname, release, machine = _osx_support.get_platform_osx(
818-
get_config_vars(),
819-
osname, release, machine)
819+
if sys.platform == "ios":
820+
release = get_config_vars().get("IPHONEOS_DEPLOYMENT_TARGET", "12.0")
821+
osname = sys.platform
822+
machine = sys.implementation._multiarch
823+
else:
824+
import _osx_support
825+
osname, release, machine = _osx_support.get_platform_osx(
826+
get_config_vars(),
827+
osname, release, machine)
820828

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

Lib/test/pythoninfo.py

+1
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ def format_groups(groups):
287287
"HOMEDRIVE",
288288
"HOMEPATH",
289289
"IDLESTARTUP",
290+
"IPHONEOS_DEPLOYMENT_TARGET",
290291
"LANG",
291292
"LDFLAGS",
292293
"LDSHARED",

Lib/test/test_concurrent_futures/test_thread_pool.py

+1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ def test_idle_thread_reuse(self):
4949
self.assertEqual(len(executor._threads), 1)
5050
executor.shutdown(wait=True)
5151

52+
@support.requires_fork()
5253
@unittest.skipUnless(hasattr(os, 'register_at_fork'), 'need os.register_at_fork')
5354
@support.requires_resource('cpu')
5455
def test_hang_global_shutdown_lock(self):

Lib/test/test_gc.py

+1
Original file line numberDiff line numberDiff line change
@@ -1188,6 +1188,7 @@ def test_collect_garbage(self):
11881188
self.assertEqual(len(gc.garbage), 0)
11891189

11901190

1191+
@requires_subprocess()
11911192
@unittest.skipIf(BUILD_WITH_NDEBUG,
11921193
'built with -NDEBUG')
11931194
def test_refcount_errors(self):

Lib/test/test_interpreters.py

+1
Original file line numberDiff line numberDiff line change
@@ -752,6 +752,7 @@ def test_sys_path_0(self):
752752

753753
class FinalizationTests(TestBase):
754754

755+
@support.requires_subprocess()
755756
def test_gh_109793(self):
756757
import subprocess
757758
argv = [sys.executable, '-c', '''if True:

Lib/test/test_lib2to3/test_parser.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,7 @@ def test_load_grammar_from_pickle(self):
6262
shutil.rmtree(tmpdir)
6363

6464
@unittest.skipIf(sys.executable is None, 'sys.executable required')
65-
@unittest.skipIf(
66-
sys.platform in {'emscripten', 'wasi'}, 'requires working subprocess'
67-
)
65+
@test.support.requires_subprocess()
6866
def test_load_grammar_from_subprocess(self):
6967
tmpdir = tempfile.mkdtemp()
7068
tmpsubdir = os.path.join(tmpdir, 'subdir')

0 commit comments

Comments
 (0)