Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
760751b
Mark a concurrency test as requiring fork.
freakboy3742 Mar 16, 2024
b41f9a4
Lower the marshalling stack depth for iOS.
freakboy3742 Mar 16, 2024
0f3e9bc
Add iOS webbrowser support.
freakboy3742 Mar 19, 2024
1a9d965
Updates to site and sysconfig modules for iOS support.
freakboy3742 Mar 19, 2024
c7fe185
Update platform module to provide ios_ver.
freakboy3742 Mar 19, 2024
732c4da
Add changenote.
freakboy3742 Mar 19, 2024
97a081d
Correct some consistency issues.
freakboy3742 Mar 19, 2024
95c367d
Modify testios Makefile target to output failures once.
freakboy3742 Mar 19, 2024
3d6d875
Avoid calling iOS APIs on tvOS/watchOS.
freakboy3742 Mar 19, 2024
d12cfa0
Add a record for the new stdlib module.
freakboy3742 Mar 20, 2024
95d11fb
Add protection against running platform.ios_ver on macOS/Linux, which…
freakboy3742 Mar 20, 2024
48f4c1a
Improve markup in the description of ios_ver()
freakboy3742 Mar 20, 2024
f584d29
Use a namedtuple for ios_ver().
freakboy3742 Mar 20, 2024
9515f75
Ensure the minimum iOS version is used in sysconfig.get_platform()
freakboy3742 Mar 20, 2024
cf0b5ff
Apply suggestions from code review
freakboy3742 Mar 21, 2024
96aa042
Correct the documentation of ios_ver()
freakboy3742 Mar 22, 2024
24b3662
Clarified some discrepancies in the platform module.
freakboy3742 Mar 22, 2024
a4e09c9
Simplify getpath handling.
freakboy3742 Mar 22, 2024
41a3c1a
Modify webbrowser module to use _ios_support as a helper.
freakboy3742 Mar 22, 2024
84ba760
Simplify sysconfig schemes for iOS.
freakboy3742 Mar 22, 2024
4194849
Apply suggestions from code review
freakboy3742 Mar 24, 2024
9a91933
More documentation tweaks.
freakboy3742 Mar 24, 2024
e6550b7
Add protection for missing ctypes to webbrowser module.
freakboy3742 Mar 24, 2024
8654376
Simplifications and clarifications to sysconfig.
freakboy3742 Mar 25, 2024
7a4dcaf
Merge branch 'main' into ios-platform-changes
freakboy3742 Mar 25, 2024
4451326
Correct the naming of the IPHONEOS_DEPLOYMENT_TARGET variable.
freakboy3742 Mar 25, 2024
4386a7a
Added clarifying comment around IPHONEOS_DEPLOYMENT_TARGET
freakboy3742 Mar 25, 2024
c1a1f0b
Merge branch 'main' into ios-platform-changes
freakboy3742 Mar 27, 2024
61559ac
Removed the hard-coded development team.
freakboy3742 Mar 27, 2024
abc2034
Account for some testing edge cases picked up in review.
freakboy3742 Mar 27, 2024
096078a
Disable --with-ensurepip for iOS.
freakboy3742 Mar 27, 2024
44bbf79
Merge branch 'main' into ios-platform-changes
freakboy3742 Mar 27, 2024
7419002
Correct merge of test_platform.
freakboy3742 Mar 27, 2024
c121d5f
Always display iOS test output.
freakboy3742 Mar 28, 2024
1ac4b26
Make test suite resilient to the absence of ctypes.
freakboy3742 Mar 28, 2024
857a0c3
Make webbrowser skip resilient across platforms.
freakboy3742 Mar 28, 2024
aa65dc2
Account for ABI suffix in header directory
ned-deily Mar 28, 2024
61e51ff
test_refcount_errors requires subprocess
ned-deily Mar 28, 2024
2ee4aba
Merge branch 'main' into ios-platform-changes
ned-deily Mar 28, 2024
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
Prev Previous commit
Next Next commit
Add iOS webbrowser support.
  • Loading branch information
freakboy3742 committed Mar 19, 2024
commit 0f3e9bc72d6124edbdcf5a9562a119f1d4c8b5ef
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 was added.

Here are some simple examples::

url = 'https://docs.python.org/'
Expand Down
81 changes: 79 additions & 2 deletions Lib/test/test_webbrowser.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@
import subprocess
from unittest import mock
from test import support
from test.support import is_apple_mobile
from test.support import import_helper
from test.support import os_helper
from test.support import requires_subprocess
from test.support import threading_helper

if not support.has_subprocess_support:
raise unittest.SkipTest("test webserver requires subprocess")
# The webbrowser module uses threading locks
threading_helper.requires_working_threading(module=True)

URL = 'https://www.example.com'
CMD_NAME = 'test'
Expand All @@ -24,6 +27,7 @@ def wait(self, seconds=None):
return 0


@requires_subprocess()
class CommandTestMixin:

def _test(self, meth, *, args=[URL], kw={}, options, arguments):
Expand Down Expand Up @@ -219,6 +223,71 @@ def test_open_new_tab(self):
arguments=['openURL({},new-tab)'.format(URL)])


@unittest.skipUnless(sys.platform == "ios", "Test only applicable to iOS")
class IOSBrowserTest(unittest.TestCase):
def _obj_ref(self, *args):
# Construct a string representation of the arguments that can be used
# as a proxy for object instance references
return "|".join(str(a) for a in args)

def setUp(self):
# Intercept the the objc library. Wrap the calls to get the
# references to classes and selectors to return strings, and
# wrap msgSend to return stringified object references
self.orig_objc = webbrowser.objc

webbrowser.objc = mock.Mock()
webbrowser.objc.objc_getClass = lambda cls: f"C#{cls.decode()}"
webbrowser.objc.sel_registerName = lambda sel: f"S#{sel.decode()}"
webbrowser.objc.objc_msgSend.side_effect = self._obj_ref

def tearDown(self):
webbrowser.objc = self.orig_objc

def _test(self, meth, **kwargs):
# The browser always gets focus, there's no concept of separate browser
# windows, and there's no API-level control over creating a new tab.
# Therefore, all calls to webbrowser are effectively the same.
getattr(webbrowser, meth)(URL, **kwargs)

# The ObjC String version of the URL is created with UTF-8 encoding
url_string_args = [
"C#NSString",
"S#stringWithCString:encoding:",
b'https://www.example.com',
4,
]
# The NSURL version of the URL is created from that string
url_obj_args = [
"C#NSURL",
"S#URLWithString:",
self._obj_ref(*url_string_args),
]
# The openURL call is invoked on the shared application
shared_app_args = ["C#UIApplication", "S#sharedApplication"]

# Verify that the last call is the one that opens the URL.
webbrowser.objc.objc_msgSend.assert_called_with(
self._obj_ref(*shared_app_args),
"S#openURL:options:completionHandler:",
self._obj_ref(*url_obj_args),
None,
None
)

def test_open(self):
self._test('open')

def test_open_with_autoraise_false(self):
self._test('open', autoraise=False)

def test_open_new(self):
self._test('open_new')

def test_open_new_tab(self):
self._test('open_new_tab')


class BrowserRegistrationTest(unittest.TestCase):

def setUp(self):
Expand Down Expand Up @@ -314,6 +383,10 @@ def test_synthesize(self):
webbrowser.register(name, None, webbrowser.GenericBrowser(name))
webbrowser.get(sys.executable)

@unittest.skipIf(
is_apple_mobile,
"Apple mobile doesn't allow modifying browser with environment"
)
def test_environment(self):
webbrowser = import_helper.import_fresh_module('webbrowser')
try:
Expand All @@ -325,6 +398,10 @@ def test_environment(self):
webbrowser = import_helper.import_fresh_module('webbrowser')
webbrowser.get()

@unittest.skipIf(
is_apple_mobile,
"Apple mobile doesn't allow modifying browser with environment"
)
def test_environment_preferred(self):
webbrowser = import_helper.import_fresh_module('webbrowser')
try:
Expand Down
77 changes: 77 additions & 0 deletions Lib/webbrowser.py
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,9 @@ def register_standard_browsers():
# OS X can use below Unix support (but we prefer using the OS X
# specific stuff)

if sys.platform == "ios":
register("iosbrowser", None, IOSBrowser(), preferred=True)

if sys.platform == "serenityos":
# SerenityOS webbrowser, simply called "Browser".
register("Browser", None, BackgroundBrowser("Browser"))
Expand Down Expand Up @@ -599,6 +602,80 @@ def open(self, url, new=0, autoraise=True):
rc = osapipe.close()
return not rc

#
# Platform support for iOS
#
if sys.platform == "ios":
try:
from ctypes import cdll, c_void_p, c_char_p, c_ulong
from ctypes import util
except ImportError:
# If ctypes isn't available, we can't trigger the browser
objc = None
else:
# ctypes is available. Load the ObjC library, and wrap the
# objc_getClass, sel_registerName and objc_msgSend methods
objc = cdll.LoadLibrary(util.find_library(b"objc"))
if objc:
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]
# The return type of objc_msgSend is always c_void_p; but the
# argument types vary with the specific call.
objc.objc_msgSend.restype = c_void_p

class IOSBrowser(BaseBrowser):
def open(self, url, new=0, autoraise=True):
sys.audit("webbrowser.open", url)
# If ctypes isn't available, we can't open a browser
if objc is None:
return False

# This is the equivalent of:
# NSString url_string =
# [NSString stringWithCString:url.encode("utf-8")
# encoding:NSUTF8StringEncoding];
NSString = objc.objc_getClass(b"NSString")
Copy link
Member

Choose a reason for hiding this comment

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

snake_case for this and the other variables here, or is it more important to mirror the objc names?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As with the _ios_ver() implementation, this is a "weak Hungarian" signifier of their ObjC origin.

constructor = objc.sel_registerName(b"stringWithCString:encoding:")
objc.objc_msgSend.argtypes = [c_void_p, c_void_p, c_char_p, c_ulong]
url_string = objc.objc_msgSend(
NSString,
constructor,
url.encode("utf-8"),
4, # NSUTF8StringEncoding = 4
)

# Create an NSURL object representing the URL
# This is the equivalent of:
# NSURL *nsurl = [NSURL URLWithString:url];
NSURL = objc.objc_getClass(b"NSURL")
urlWithString_ = objc.sel_registerName(b"URLWithString:")
objc.objc_msgSend.argtypes = [c_void_p, c_void_p, c_void_p]
ns_url = objc.objc_msgSend(NSURL, urlWithString_, url_string)

# Get the shared UIApplication instance
# This code is the equivalent of:
# UIApplication shared_app = [UIApplication sharedApplication]
UIApplication = objc.objc_getClass(b"UIApplication")
sharedApplication = objc.sel_registerName(b"sharedApplication")
objc.objc_msgSend.argtypes = [c_void_p, c_void_p]
shared_app = objc.objc_msgSend(UIApplication, sharedApplication)

# Open the URL on the shared application
# This code is the equivalent of:
# [shared_app openURL:ns_url
# options:NIL
# completionHandler:NIL];
openURL_ = objc.sel_registerName(b"openURL:options:completionHandler:")
objc.objc_msgSend.argtypes = [
c_void_p, c_void_p, c_void_p, c_void_p, c_void_p
]
objc.objc_msgSend.restype = None
objc.objc_msgSend(shared_app, openURL_, ns_url, None, None)

return True


def main():
import getopt
Expand Down