diff --git a/Lib/test/test_webbrowser.py b/Lib/test/test_webbrowser.py
index 673cc995d..4fcbc5c2e 100644
--- a/Lib/test/test_webbrowser.py
+++ b/Lib/test/test_webbrowser.py
@@ -1,15 +1,22 @@
-import webbrowser
-import unittest
import os
-import sys
+import re
+import shlex
import subprocess
-from unittest import mock
+import sys
+import unittest
+import webbrowser
from test import support
from test.support import import_helper
+from test.support import is_apple_mobile
from test.support import os_helper
+from test.support import requires_subprocess
+from test.support import threading_helper
+from unittest import mock
+# The webbrowser module uses threading locks
+threading_helper.requires_working_threading(module=True)
-URL = 'http://www.example.com'
+URL = 'https://www.example.com'
CMD_NAME = 'test'
@@ -22,6 +29,7 @@ class PopenMock(mock.MagicMock):
return 0
+@requires_subprocess()
class CommandTestMixin:
def _test(self, meth, *, args=[URL], kw={}, options, arguments):
@@ -92,6 +100,40 @@ class ChromeCommandTest(CommandTestMixin, unittest.TestCase):
options=[],
arguments=[URL])
+ def test_open_bad_new_parameter(self):
+ with self.assertRaisesRegex(webbrowser.Error,
+ re.escape("Bad 'new' parameter to open(); "
+ "expected 0, 1, or 2, got 999")):
+ self._test('open',
+ options=[],
+ arguments=[URL],
+ kw=dict(new=999))
+
+
+class EdgeCommandTest(CommandTestMixin, unittest.TestCase):
+
+ browser_class = webbrowser.Edge
+
+ def test_open(self):
+ self._test('open',
+ options=[],
+ arguments=[URL])
+
+ def test_open_with_autoraise_false(self):
+ self._test('open', kw=dict(autoraise=False),
+ options=[],
+ arguments=[URL])
+
+ def test_open_new(self):
+ self._test('open_new',
+ options=['--new-window'],
+ arguments=[URL])
+
+ def test_open_new_tab(self):
+ self._test('open_new_tab',
+ options=[],
+ arguments=[URL])
+
class MozillaCommandTest(CommandTestMixin, unittest.TestCase):
@@ -118,34 +160,9 @@ class MozillaCommandTest(CommandTestMixin, unittest.TestCase):
arguments=['-new-tab', URL])
-class NetscapeCommandTest(CommandTestMixin, unittest.TestCase):
+class EpiphanyCommandTest(CommandTestMixin, unittest.TestCase):
- browser_class = webbrowser.Netscape
-
- def test_open(self):
- self._test('open',
- options=['-raise', '-remote'],
- arguments=['openURL({})'.format(URL)])
-
- def test_open_with_autoraise_false(self):
- self._test('open', kw=dict(autoraise=False),
- options=['-noraise', '-remote'],
- arguments=['openURL({})'.format(URL)])
-
- def test_open_new(self):
- self._test('open_new',
- options=['-raise', '-remote'],
- arguments=['openURL({},new-window)'.format(URL)])
-
- def test_open_new_tab(self):
- self._test('open_new_tab',
- options=['-raise', '-remote'],
- arguments=['openURL({},new-tab)'.format(URL)])
-
-
-class GaleonCommandTest(CommandTestMixin, unittest.TestCase):
-
- browser_class = webbrowser.Galeon
+ browser_class = webbrowser.Epiphany
def test_open(self):
self._test('open',
@@ -199,22 +216,89 @@ class ELinksCommandTest(CommandTestMixin, unittest.TestCase):
def test_open(self):
self._test('open', options=['-remote'],
- arguments=['openURL({})'.format(URL)])
+ arguments=[f'openURL({URL})'])
def test_open_with_autoraise_false(self):
self._test('open',
options=['-remote'],
- arguments=['openURL({})'.format(URL)])
+ arguments=[f'openURL({URL})'])
def test_open_new(self):
self._test('open_new',
options=['-remote'],
- arguments=['openURL({},new-window)'.format(URL)])
+ arguments=[f'openURL({URL},new-window)'])
def test_open_new_tab(self):
self._test('open_new_tab',
options=['-remote'],
- arguments=['openURL({},new-tab)'.format(URL)])
+ arguments=[f'openURL({URL},new-tab)'])
+
+
+@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)
+
+ @unittest.skipIf(getattr(webbrowser, "objc", None) is None,
+ "iOS Webbrowser tests require ctypes")
+ def setUp(self):
+ # Intercept 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):
@@ -269,6 +353,16 @@ class BrowserRegistrationTest(unittest.TestCase):
def test_register_preferred(self):
self._check_registration(preferred=True)
+ @unittest.skipUnless(sys.platform == "darwin", "macOS specific test")
+ def test_no_xdg_settings_on_macOS(self):
+ # On macOS webbrowser should not use xdg-settings to
+ # look for X11 based browsers (for those users with
+ # XQuartz installed)
+ with mock.patch("subprocess.check_output") as ck_o:
+ webbrowser.register_standard_browsers()
+
+ ck_o.assert_not_called()
+
class ImportTest(unittest.TestCase):
def test_register(self):
@@ -294,29 +388,38 @@ class ImportTest(unittest.TestCase):
webbrowser.get('fakebrowser')
self.assertIsNotNone(webbrowser._tryorder)
+ @unittest.skipIf(" " in sys.executable, "test assumes no space in path (GH-114452)")
def test_synthesize(self):
webbrowser = import_helper.import_fresh_module('webbrowser')
name = os.path.basename(sys.executable).lower()
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:
browser = webbrowser.get().name
- except (webbrowser.Error, AttributeError) as err:
+ except webbrowser.Error as err:
self.skipTest(str(err))
with os_helper.EnvironmentVarGuard() as env:
env["BROWSER"] = browser
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:
webbrowser.get()
least_preferred_browser = webbrowser.get(webbrowser._tryorder[-1]).name
- except (webbrowser.Error, AttributeError, IndexError) as err:
+ except (webbrowser.Error, IndexError) as err:
self.skipTest(str(err))
with os_helper.EnvironmentVarGuard() as env:
@@ -330,5 +433,74 @@ class ImportTest(unittest.TestCase):
self.assertEqual(webbrowser.get().name, sys.executable)
-if __name__=='__main__':
+class CliTest(unittest.TestCase):
+ def test_parse_args(self):
+ for command, url, new_win in [
+ # No optional arguments
+ ("https://example.com", "https://example.com", 0),
+ # Each optional argument
+ ("https://example.com -n", "https://example.com", 1),
+ ("-n https://example.com", "https://example.com", 1),
+ ("https://example.com -t", "https://example.com", 2),
+ ("-t https://example.com", "https://example.com", 2),
+ # Long form
+ ("https://example.com --new-window", "https://example.com", 1),
+ ("--new-window https://example.com", "https://example.com", 1),
+ ("https://example.com --new-tab", "https://example.com", 2),
+ ("--new-tab https://example.com", "https://example.com", 2),
+ ]:
+ args = webbrowser.parse_args(shlex.split(command))
+
+ self.assertEqual(args.url, url)
+ self.assertEqual(args.new_win, new_win)
+
+ def test_parse_args_error(self):
+ for command in [
+ # Arguments must not both be given
+ "https://example.com -n -t",
+ "https://example.com --new-window --new-tab",
+ "https://example.com -n --new-tab",
+ "https://example.com --new-window -t",
+ ]:
+ with support.captured_stderr() as stderr:
+ with self.assertRaises(SystemExit):
+ webbrowser.parse_args(shlex.split(command))
+ self.assertIn(
+ 'error: argument -t/--new-tab: not allowed with argument -n/--new-window',
+ stderr.getvalue(),
+ )
+
+ # Ensure ambiguous shortening fails
+ with support.captured_stderr() as stderr:
+ with self.assertRaises(SystemExit):
+ webbrowser.parse_args(shlex.split("https://example.com --new"))
+ self.assertIn(
+ 'error: ambiguous option: --new could match --new-window, --new-tab',
+ stderr.getvalue()
+ )
+
+ def test_main(self):
+ for command, expected_url, expected_new_win in [
+ # No optional arguments
+ ("https://example.com", "https://example.com", 0),
+ # Each optional argument
+ ("https://example.com -n", "https://example.com", 1),
+ ("-n https://example.com", "https://example.com", 1),
+ ("https://example.com -t", "https://example.com", 2),
+ ("-t https://example.com", "https://example.com", 2),
+ # Long form
+ ("https://example.com --new-window", "https://example.com", 1),
+ ("--new-window https://example.com", "https://example.com", 1),
+ ("https://example.com --new-tab", "https://example.com", 2),
+ ("--new-tab https://example.com", "https://example.com", 2),
+ ]:
+ with (
+ mock.patch("webbrowser.open", return_value=None) as mock_open,
+ mock.patch("builtins.print", return_value=None),
+ ):
+ webbrowser.main(shlex.split(command))
+ mock_open.assert_called_once_with(expected_url, expected_new_win)
+
+
+if __name__ == '__main__':
unittest.main()
diff --git a/Lib/webbrowser.py b/Lib/webbrowser.py
index ec3cece48..2f9555ad6 100755
--- a/Lib/webbrowser.py
+++ b/Lib/webbrowser.py
@@ -11,14 +11,17 @@ import threading
__all__ = ["Error", "open", "open_new", "open_new_tab", "get", "register"]
+
class Error(Exception):
pass
+
_lock = threading.RLock()
_browsers = {} # Dictionary of available browser controllers
_tryorder = None # Preference order of available browsers
_os_preferred_browser = None # The preferred browser
+
def register(name, klass, instance=None, *, preferred=False):
"""Register a browser connector."""
with _lock:
@@ -29,11 +32,12 @@ def register(name, klass, instance=None, *, preferred=False):
# Preferred browsers go to the front of the list.
# Need to match to the default browser returned by xdg-settings, which
# may be of the form e.g. "firefox.desktop".
- if preferred or (_os_preferred_browser and name in _os_preferred_browser):
+ if preferred or (_os_preferred_browser and f'{name}.desktop' == _os_preferred_browser):
_tryorder.insert(0, name)
else:
_tryorder.append(name)
+
def get(using=None):
"""Return a browser launcher instance appropriate for the environment."""
if _tryorder is None:
@@ -64,6 +68,7 @@ def get(using=None):
return command[0]()
raise Error("could not locate runnable browser")
+
# Please note: the following definition hides a builtin function.
# It is recommended one does "import webbrowser" and uses webbrowser.open(url)
# instead of "from webbrowser import *".
@@ -76,6 +81,9 @@ def open(url, new=0, autoraise=True):
- 1: a new browser window.
- 2: a new browser page ("tab").
If possible, autoraise raises the window (the default) or not.
+
+ If opening the browser succeeds, return True.
+ If there is a problem, return False.
"""
if _tryorder is None:
with _lock:
@@ -87,6 +95,7 @@ def open(url, new=0, autoraise=True):
return True
return False
+
def open_new(url):
"""Open url in a new window of the default browser.
@@ -94,6 +103,7 @@ def open_new(url):
"""
return open(url, 1)
+
def open_new_tab(url):
"""Open url in a new page ("tab") of the default browser.
@@ -136,7 +146,7 @@ def _synthesize(browser, *, preferred=False):
# General parent classes
-class BaseBrowser(object):
+class BaseBrowser:
"""Parent class for all browsers. Do not use directly."""
args = ['%s']
@@ -197,7 +207,7 @@ class BackgroundBrowser(GenericBrowser):
else:
p = subprocess.Popen(cmdline, close_fds=True,
start_new_session=True)
- return (p.poll() is None)
+ return p.poll() is None
except OSError:
return False
@@ -225,7 +235,8 @@ class UnixBrowser(BaseBrowser):
# use autoraise argument only for remote invocation
autoraise = int(autoraise)
opt = self.raise_opts[autoraise]
- if opt: raise_opt = [opt]
+ if opt:
+ raise_opt = [opt]
cmdline = [self.name] + raise_opt + args
@@ -266,8 +277,8 @@ class UnixBrowser(BaseBrowser):
else:
action = self.remote_action_newtab
else:
- raise Error("Bad 'new' parameter to open(); " +
- "expected 0, 1, or 2, got %s" % new)
+ raise Error("Bad 'new' parameter to open(); "
+ f"expected 0, 1, or 2, got {new}")
args = [arg.replace("%s", url).replace("%action", action)
for arg in self.remote_args]
@@ -291,19 +302,8 @@ class Mozilla(UnixBrowser):
background = True
-class Netscape(UnixBrowser):
- """Launcher class for Netscape browser."""
-
- raise_opts = ["-noraise", "-raise"]
- remote_args = ['-remote', 'openURL(%s%action)']
- remote_action = ""
- remote_action_newwin = ",new-window"
- remote_action_newtab = ",new-tab"
- background = True
-
-
-class Galeon(UnixBrowser):
- """Launcher class for Galeon/Epiphany browsers."""
+class Epiphany(UnixBrowser):
+ """Launcher class for Epiphany browser."""
raise_opts = ["-noraise", ""]
remote_args = ['%action', '%s']
@@ -313,7 +313,7 @@ class Galeon(UnixBrowser):
class Chrome(UnixBrowser):
- "Launcher class for Google Chrome browser."
+ """Launcher class for Google Chrome browser."""
remote_args = ['%action', '%s']
remote_action = ""
@@ -321,11 +321,12 @@ class Chrome(UnixBrowser):
remote_action_newtab = ""
background = True
+
Chromium = Chrome
class Opera(UnixBrowser):
- "Launcher class for Opera browser."
+ """Launcher class for Opera browser."""
remote_args = ['%action', '%s']
remote_action = ""
@@ -335,7 +336,7 @@ class Opera(UnixBrowser):
class Elinks(UnixBrowser):
- "Launcher class for Elinks browsers."
+ """Launcher class for Elinks browsers."""
remote_args = ['-remote', 'openURL(%s%action)']
remote_action = ""
@@ -398,54 +399,17 @@ class Konqueror(BaseBrowser):
except OSError:
return False
else:
- return (p.poll() is None)
+ return p.poll() is None
-class Grail(BaseBrowser):
- # There should be a way to maintain a connection to Grail, but the
- # Grail remote control protocol doesn't really allow that at this
- # point. It probably never will!
- def _find_grail_rc(self):
- import glob
- import pwd
- import socket
- import tempfile
- tempdir = os.path.join(tempfile.gettempdir(),
- ".grail-unix")
- user = pwd.getpwuid(os.getuid())[0]
- filename = os.path.join(glob.escape(tempdir), glob.escape(user) + "-*")
- maybes = glob.glob(filename)
- if not maybes:
- return None
- s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
- for fn in maybes:
- # need to PING each one until we find one that's live
- try:
- s.connect(fn)
- except OSError:
- # no good; attempt to clean it out, but don't fail:
- try:
- os.unlink(fn)
- except OSError:
- pass
- else:
- return s
+class Edge(UnixBrowser):
+ """Launcher class for Microsoft Edge browser."""
- def _remote(self, action):
- s = self._find_grail_rc()
- if not s:
- return 0
- s.send(action)
- s.close()
- return 1
-
- def open(self, url, new=0, autoraise=True):
- sys.audit("webbrowser.open", url)
- if new:
- ok = self._remote("LOADNEW " + url)
- else:
- ok = self._remote("LOAD " + url)
- return ok
+ remote_args = ['%action', '%s']
+ remote_action = ""
+ remote_action_newwin = "--new-window"
+ remote_action_newtab = ""
+ background = True
#
@@ -461,47 +425,44 @@ def register_X_browsers():
if shutil.which("xdg-open"):
register("xdg-open", None, BackgroundBrowser("xdg-open"))
+ # Opens an appropriate browser for the URL scheme according to
+ # freedesktop.org settings (GNOME, KDE, XFCE, etc.)
+ if shutil.which("gio"):
+ register("gio", None, BackgroundBrowser(["gio", "open", "--", "%s"]))
+
+ xdg_desktop = os.getenv("XDG_CURRENT_DESKTOP", "").split(":")
+
# The default GNOME3 browser
- if "GNOME_DESKTOP_SESSION_ID" in os.environ and shutil.which("gvfs-open"):
+ if (("GNOME" in xdg_desktop or
+ "GNOME_DESKTOP_SESSION_ID" in os.environ) and
+ shutil.which("gvfs-open")):
register("gvfs-open", None, BackgroundBrowser("gvfs-open"))
- # The default GNOME browser
- if "GNOME_DESKTOP_SESSION_ID" in os.environ and shutil.which("gnome-open"):
- register("gnome-open", None, BackgroundBrowser("gnome-open"))
-
# The default KDE browser
- if "KDE_FULL_SESSION" in os.environ and shutil.which("kfmclient"):
+ if (("KDE" in xdg_desktop or
+ "KDE_FULL_SESSION" in os.environ) and
+ shutil.which("kfmclient")):
register("kfmclient", Konqueror, Konqueror("kfmclient"))
+ # Common symbolic link for the default X11 browser
if shutil.which("x-www-browser"):
register("x-www-browser", None, BackgroundBrowser("x-www-browser"))
# The Mozilla browsers
- for browser in ("firefox", "iceweasel", "iceape", "seamonkey"):
+ for browser in ("firefox", "iceweasel", "seamonkey", "mozilla-firefox",
+ "mozilla"):
if shutil.which(browser):
register(browser, None, Mozilla(browser))
- # The Netscape and old Mozilla browsers
- for browser in ("mozilla-firefox",
- "mozilla-firebird", "firebird",
- "mozilla", "netscape"):
- if shutil.which(browser):
- register(browser, None, Netscape(browser))
-
# Konqueror/kfm, the KDE browser.
if shutil.which("kfm"):
register("kfm", Konqueror, Konqueror("kfm"))
elif shutil.which("konqueror"):
register("konqueror", Konqueror, Konqueror("konqueror"))
- # Gnome's Galeon and Epiphany
- for browser in ("galeon", "epiphany"):
- if shutil.which(browser):
- register(browser, None, Galeon(browser))
-
- # Skipstone, another Gtk/Mozilla based browser
- if shutil.which("skipstone"):
- register("skipstone", None, BackgroundBrowser("skipstone"))
+ # Gnome's Epiphany
+ if shutil.which("epiphany"):
+ register("epiphany", None, Epiphany("epiphany"))
# Google Chrome/Chromium browsers
for browser in ("google-chrome", "chrome", "chromium", "chromium-browser"):
@@ -512,13 +473,9 @@ def register_X_browsers():
if shutil.which("opera"):
register("opera", None, Opera("opera"))
- # Next, Mosaic -- old but still in use.
- if shutil.which("mosaic"):
- register("mosaic", None, BackgroundBrowser("mosaic"))
+ if shutil.which("microsoft-edge"):
+ register("microsoft-edge", None, Edge("microsoft-edge"))
- # Grail, the Python browser. Does anybody still use it?
- if shutil.which("grail"):
- register("grail", Grail, None)
def register_standard_browsers():
global _tryorder
@@ -532,6 +489,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"))
@@ -540,21 +500,33 @@ def register_standard_browsers():
# First try to use the default Windows browser
register("windows-default", WindowsDefault)
- # Detect some common Windows browsers, fallback to IE
- iexplore = os.path.join(os.environ.get("PROGRAMFILES", "C:\\Program Files"),
- "Internet Explorer\\IEXPLORE.EXE")
- for browser in ("firefox", "firebird", "seamonkey", "mozilla",
- "netscape", "opera", iexplore):
+ # Detect some common Windows browsers, fallback to Microsoft Edge
+ # location in 64-bit Windows
+ edge64 = os.path.join(os.environ.get("PROGRAMFILES(x86)", "C:\\Program Files (x86)"),
+ "Microsoft\\Edge\\Application\\msedge.exe")
+ # location in 32-bit Windows
+ edge32 = os.path.join(os.environ.get("PROGRAMFILES", "C:\\Program Files"),
+ "Microsoft\\Edge\\Application\\msedge.exe")
+ for browser in ("firefox", "seamonkey", "mozilla", "chrome",
+ "opera", edge64, edge32):
if shutil.which(browser):
register(browser, None, BackgroundBrowser(browser))
+ if shutil.which("MicrosoftEdge.exe"):
+ register("microsoft-edge", None, Edge("MicrosoftEdge.exe"))
else:
# Prefer X browsers if present
- if os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY"):
+ #
+ # NOTE: Do not check for X11 browser on macOS,
+ # XQuartz installation sets a DISPLAY environment variable and will
+ # autostart when someone tries to access the display. Mac users in
+ # general don't need an X11 browser.
+ if sys.platform != "darwin" and (os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY")):
try:
cmd = "xdg-settings get default-web-browser".split()
raw_result = subprocess.check_output(cmd, stderr=subprocess.DEVNULL)
result = raw_result.decode().strip()
- except (FileNotFoundError, subprocess.CalledProcessError, PermissionError, NotADirectoryError) :
+ except (FileNotFoundError, subprocess.CalledProcessError,
+ PermissionError, NotADirectoryError):
pass
else:
global _os_preferred_browser
@@ -564,14 +536,15 @@ def register_standard_browsers():
# Also try console browsers
if os.environ.get("TERM"):
+ # Common symbolic link for the default text-based browser
if shutil.which("www-browser"):
register("www-browser", None, GenericBrowser("www-browser"))
- # The Links/elinks browsers
+ # The Links/elinks browsers
if shutil.which("links"):
register("links", None, GenericBrowser("links"))
if shutil.which("elinks"):
register("elinks", None, Elinks("elinks"))
- # The Lynx browser ,
+ # The Lynx browser ,
if shutil.which("lynx"):
register("lynx", None, GenericBrowser("lynx"))
# The w3m browser
@@ -613,72 +586,26 @@ if sys.platform[:3] == "win":
return True
#
-# Platform support for MacOS
+# Platform support for macOS
#
if sys.platform == 'darwin':
- # Adapted from patch submitted to SourceForge by Steven J. Burr
- class MacOSX(BaseBrowser):
- """Launcher class for Aqua browsers on Mac OS X
-
- Optionally specify a browser name on instantiation. Note that this
- will not work for Aqua browsers if the user has moved the application
- package after installation.
-
- If no browser is specified, the default browser, as specified in the
- Internet System Preferences panel, will be used.
- """
- def __init__(self, name):
- self.name = name
+ class MacOSXOSAScript(BaseBrowser):
+ def __init__(self, name='default'):
+ super().__init__(name)
def open(self, url, new=0, autoraise=True):
sys.audit("webbrowser.open", url)
- assert "'" not in url
- # hack for local urls
- if not ':' in url:
- url = 'file:'+url
-
- # new must be 0 or 1
- new = int(bool(new))
- if self.name == "default":
- # User called open, open_new or get without a browser parameter
- script = 'open location "%s"' % url.replace('"', '%22') # opens in default browser
+ url = url.replace('"', '%22')
+ if self.name == 'default':
+ script = f'open location "{url}"' # opens in default browser
else:
- # User called get and chose a browser
- if self.name == "OmniWeb":
- toWindow = ""
- else:
- # Include toWindow parameter of OpenURL command for browsers
- # that support it. 0 == new window; -1 == existing
- toWindow = "toWindow %d" % (new - 1)
- cmd = 'OpenURL "%s"' % url.replace('"', '%22')
- script = '''tell application "%s"
- activate
- %s %s
- end tell''' % (self.name, cmd, toWindow)
- # Open pipe to AppleScript through osascript command
- osapipe = os.popen("osascript", "w")
- if osapipe is None:
- return False
- # Write script to osascript's stdin
- osapipe.write(script)
- rc = osapipe.close()
- return not rc
-
- class MacOSXOSAScript(BaseBrowser):
- def __init__(self, name):
- self._name = name
-
- def open(self, url, new=0, autoraise=True):
- if self._name == 'default':
- script = 'open location "%s"' % url.replace('"', '%22') # opens in default browser
- else:
- script = '''
- tell application "%s"
+ script = f'''
+ tell application "{self.name}"
activate
- open location "%s"
+ open location "{url}"
end
- '''%(self._name, url.replace('"', '%22'))
+ '''
osapipe = os.popen("osascript", "w")
if osapipe is None:
@@ -688,30 +615,96 @@ if sys.platform == 'darwin':
rc = osapipe.close()
return not rc
+#
+# Platform support for iOS
+#
+if sys.platform == "ios":
+ from _ios_support import objc
+ if objc:
+ # If objc exists, we know ctypes is also importable.
+ from ctypes import c_void_p, c_char_p, c_ulong
-def main():
- import getopt
- usage = """Usage: %s [-n | -t] url
- -n: open new window
- -t: open new tab""" % sys.argv[0]
- try:
- opts, args = getopt.getopt(sys.argv[1:], 'ntd')
- except getopt.error as msg:
- print(msg, file=sys.stderr)
- print(usage, file=sys.stderr)
- sys.exit(1)
- new_win = 0
- for o, a in opts:
- if o == '-n': new_win = 1
- elif o == '-t': new_win = 2
- if len(args) != 1:
- print(usage, file=sys.stderr)
- sys.exit(1)
+ 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
- url = args[0]
- open(url, new_win)
+ # All the messages in this call return object references.
+ objc.objc_msgSend.restype = c_void_p
+
+ # This is the equivalent of:
+ # NSString url_string =
+ # [NSString stringWithCString:url.encode("utf-8")
+ # encoding:NSUTF8StringEncoding];
+ NSString = objc.objc_getClass(b"NSString")
+ 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
+ ]
+ # Method returns void
+ objc.objc_msgSend.restype = None
+ objc.objc_msgSend(shared_app, openURL_, ns_url, None, None)
+
+ return True
+
+
+def parse_args(arg_list: list[str] | None):
+ import argparse
+ parser = argparse.ArgumentParser(description="Open URL in a web browser.")
+ parser.add_argument("url", help="URL to open")
+
+ group = parser.add_mutually_exclusive_group()
+ group.add_argument("-n", "--new-window", action="store_const",
+ const=1, default=0, dest="new_win",
+ help="open new window")
+ group.add_argument("-t", "--new-tab", action="store_const",
+ const=2, default=0, dest="new_win",
+ help="open new tab")
+
+ args = parser.parse_args(arg_list)
+
+ return args
+
+
+def main(arg_list: list[str] | None = None):
+ args = parse_args(arg_list)
+
+ open(args.url, args.new_win)
print("\a")
+
if __name__ == "__main__":
main()