| import io | |
| import os | |
| import re | |
| import shlex | |
| import subprocess | |
| import sys | |
| import unittest | |
| import warnings | |
| import webbrowser | |
| from test import support | |
| from test.support import force_not_colorized_test_class | |
| 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 = 'https://www.example.com' | |
| CMD_NAME = 'test' | |
| class PopenMock(mock.MagicMock): | |
| def poll(self): | |
| return 0 | |
| def wait(self, seconds=None): | |
| return 0 | |
| @requires_subprocess() | |
| class CommandTestMixin: | |
| def _test(self, meth, *, args=[URL], kw={}, options, arguments): | |
| """Given a web browser instance method name along with arguments and | |
| keywords for same (which defaults to the single argument URL), creates | |
| a browser instance from the class pointed to by self.browser, calls the | |
| indicated instance method with the indicated arguments, and compares | |
| the resulting options and arguments passed to Popen by the browser | |
| instance against the 'options' and 'args' lists. Options are compared | |
| in a position independent fashion, and the arguments are compared in | |
| sequence order to whatever is left over after removing the options. | |
| """ | |
| popen = PopenMock() | |
| support.patch(self, subprocess, 'Popen', popen) | |
| browser = self.browser_class(name=CMD_NAME) | |
| getattr(browser, meth)(*args, **kw) | |
| popen_args = subprocess.Popen.call_args[0][0] | |
| self.assertEqual(popen_args[0], CMD_NAME) | |
| popen_args.pop(0) | |
| for option in options: | |
| self.assertIn(option, popen_args) | |
| popen_args.pop(popen_args.index(option)) | |
| self.assertEqual(popen_args, arguments) | |
| def test_reject_dash_prefixes(self): | |
| browser = self.browser_class(name=CMD_NAME) | |
| with self.assertRaisesRegex( | |
| ValueError, | |
| r"^Invalid URL \(leading dash disallowed\): '--key=val http.*'$" | |
| ): | |
| browser.open(f"--key=val {URL}") | |
| class GenericBrowserCommandTest(CommandTestMixin, unittest.TestCase): | |
| browser_class = webbrowser.GenericBrowser | |
| def test_open(self): | |
| self._test('open', | |
| options=[], | |
| arguments=[URL]) | |
| class BackgroundBrowserCommandTest(CommandTestMixin, unittest.TestCase): | |
| browser_class = webbrowser.BackgroundBrowser | |
| def test_open(self): | |
| self._test('open', | |
| options=[], | |
| arguments=[URL]) | |
| class ChromeCommandTest(CommandTestMixin, unittest.TestCase): | |
| browser_class = webbrowser.Chrome | |
| 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]) | |
| 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)) | |
| def test_reject_action_dash_prefixes(self): | |
| browser = self.browser_class(name=CMD_NAME) | |
| with self.assertRaises(ValueError): | |
| browser.open('%action--incognito') | |
| # new=1: action is "--new-window", so "%action" itself expands to | |
| # a dash-prefixed flag even with no dash in the original URL. | |
| with self.assertRaises(ValueError): | |
| browser.open('%action', new=1) | |
| 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): | |
| browser_class = webbrowser.Mozilla | |
| 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=[], | |
| arguments=['-new-window', URL]) | |
| def test_open_new_tab(self): | |
| self._test('open_new_tab', | |
| options=[], | |
| arguments=['-new-tab', URL]) | |
| class EpiphanyCommandTest(CommandTestMixin, unittest.TestCase): | |
| browser_class = webbrowser.Epiphany | |
| def test_open(self): | |
| self._test('open', | |
| options=['-n'], | |
| arguments=[URL]) | |
| def test_open_with_autoraise_false(self): | |
| self._test('open', kw=dict(autoraise=False), | |
| options=['-noraise', '-n'], | |
| arguments=[URL]) | |
| def test_open_new(self): | |
| self._test('open_new', | |
| options=['-w'], | |
| arguments=[URL]) | |
| def test_open_new_tab(self): | |
| self._test('open_new_tab', | |
| options=['-w'], | |
| arguments=[URL]) | |
| class OperaCommandTest(CommandTestMixin, unittest.TestCase): | |
| browser_class = webbrowser.Opera | |
| 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 ELinksCommandTest(CommandTestMixin, unittest.TestCase): | |
| browser_class = webbrowser.Elinks | |
| def test_open(self): | |
| self._test('open', options=['-remote'], | |
| arguments=[f'openURL({URL})']) | |
| def test_open_with_autoraise_false(self): | |
| self._test('open', | |
| options=['-remote'], | |
| arguments=[f'openURL({URL})']) | |
| def test_open_new(self): | |
| self._test('open_new', | |
| options=['-remote'], | |
| arguments=[f'openURL({URL},new-window)']) | |
| def test_open_new_tab(self): | |
| self._test('open_new_tab', | |
| options=['-remote'], | |
| 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 MockPopenPipe: | |
| def __init__(self, cmd, mode): | |
| self.cmd = cmd | |
| self.mode = mode | |
| self.pipe = io.StringIO() | |
| self._closed = False | |
| def write(self, buf): | |
| self.pipe.write(buf) | |
| def close(self): | |
| self._closed = True | |
| return None | |
| @unittest.skipUnless(sys.platform == "darwin", "macOS specific test") | |
| @requires_subprocess() | |
| class MacOSTest(unittest.TestCase): | |
| def setUp(self): | |
| env = self.enterContext(os_helper.EnvironmentVarGuard()) | |
| env.unset("BROWSER") | |
| def test_default(self): | |
| browser = webbrowser.get() | |
| self.assertIsInstance(browser, webbrowser.MacOS) | |
| self.assertEqual(browser.name, 'default') | |
| def test_default_http_open(self): | |
| # http/https URLs use /usr/bin/open directly — no bundle ID needed. | |
| browser = webbrowser.MacOS('default') | |
| with mock.patch('subprocess.run') as mock_run: | |
| mock_run.return_value = mock.Mock(returncode=0) | |
| result = browser.open(URL) | |
| mock_run.assert_called_once_with( | |
| ['/usr/bin/open', URL], | |
| stderr=subprocess.DEVNULL, | |
| ) | |
| self.assertTrue(result) | |
| def test_default_non_http_uses_bundle_id(self): | |
| # Non-http(s) URLs (e.g. file://) must be routed through the browser | |
| # via -b <bundle-id> to prevent OS file handler dispatch. | |
| file_url = 'file:///tmp/test.html' | |
| browser = webbrowser.MacOS('default') | |
| with mock.patch('webbrowser._macos_default_browser_bundle_id', | |
| return_value='com.google.Chrome'), \ | |
| mock.patch('subprocess.run') as mock_run: | |
| mock_run.return_value = mock.Mock(returncode=0) | |
| result = browser.open(file_url) | |
| mock_run.assert_called_once_with( | |
| ['/usr/bin/open', '-b', 'com.google.Chrome', file_url], | |
| stderr=subprocess.DEVNULL, | |
| ) | |
| self.assertTrue(result) | |
| def test_named_known_browser_uses_bundle_id(self): | |
| # Named browsers with a known bundle ID use /usr/bin/open -b. | |
| browser = webbrowser.MacOS('safari') | |
| with mock.patch('subprocess.run') as mock_run: | |
| mock_run.return_value = mock.Mock(returncode=0) | |
| result = browser.open(URL) | |
| mock_run.assert_called_once_with( | |
| ['/usr/bin/open', '-b', 'com.apple.Safari', URL], | |
| stderr=subprocess.DEVNULL, | |
| ) | |
| self.assertTrue(result) | |
| def test_named_unknown_browser_falls_back_to_dash_a(self): | |
| # Named browsers not in the bundle ID map fall back to -a. | |
| browser = webbrowser.MacOS('lynx') | |
| with mock.patch('subprocess.run') as mock_run: | |
| mock_run.return_value = mock.Mock(returncode=0) | |
| browser.open(URL) | |
| mock_run.assert_called_once_with( | |
| ['/usr/bin/open', '-a', 'lynx', URL], | |
| stderr=subprocess.DEVNULL, | |
| ) | |
| def test_open_failure(self): | |
| browser = webbrowser.MacOS('default') | |
| with mock.patch('subprocess.run') as mock_run: | |
| mock_run.return_value = mock.Mock(returncode=1) | |
| result = browser.open(URL) | |
| self.assertFalse(result) | |
| @unittest.skipUnless(sys.platform == "darwin", "macOS specific test") | |
| @requires_subprocess() | |
| class MacOSXOSAScriptDeprecationTest(unittest.TestCase): | |
| def test_deprecation_warning(self): | |
| with self.assertWarns(DeprecationWarning): | |
| webbrowser.MacOSXOSAScript('default') | |
| @unittest.skipUnless(sys.platform == "darwin", "macOS specific test") | |
| @requires_subprocess() | |
| class MacOSXOSAScriptTest(unittest.TestCase): | |
| def setUp(self): | |
| # Ensure that 'BROWSER' is not set to 'open' or something else. | |
| # See: https://github.com/python/cpython/issues/131254. | |
| env = self.enterContext(os_helper.EnvironmentVarGuard()) | |
| env.unset("BROWSER") | |
| support.patch(self, os, "popen", self.mock_popen) | |
| self.enterContext(warnings.catch_warnings()) | |
| warnings.simplefilter("ignore", DeprecationWarning) | |
| self.browser = webbrowser.MacOSXOSAScript("default") | |
| def mock_popen(self, cmd, mode): | |
| self.popen_pipe = MockPopenPipe(cmd, mode) | |
| return self.popen_pipe | |
| def test_default_open(self): | |
| url = "https://python.org" | |
| self.browser.open(url) | |
| self.assertTrue(self.popen_pipe._closed) | |
| self.assertEqual(self.popen_pipe.cmd, "/usr/bin/osascript") | |
| script = self.popen_pipe.pipe.getvalue() | |
| self.assertEqual(script.strip(), f'open location "{url}"') | |
| def test_url_quote(self): | |
| self.browser.open('https://python.org/"quote"') | |
| script = self.popen_pipe.pipe.getvalue() | |
| self.assertEqual( | |
| script.strip(), 'open location "https://python.org/%22quote%22"' | |
| ) | |
| def test_default_browser_lookup(self): | |
| url = "file:///tmp/some-file.html" | |
| self.browser.open(url) | |
| script = self.popen_pipe.pipe.getvalue() | |
| # doesn't actually test the browser lookup works, | |
| # just that the branch is taken | |
| self.assertIn("URLForApplicationToOpenURL", script) | |
| self.assertIn(f'open location "{url}"', script) | |
| def test_explicit_browser(self): | |
| with warnings.catch_warnings(): | |
| warnings.simplefilter("ignore", DeprecationWarning) | |
| browser = webbrowser.MacOSXOSAScript("safari") | |
| browser.open("https://python.org") | |
| script = self.popen_pipe.pipe.getvalue() | |
| self.assertIn('tell application "safari"', script) | |
| self.assertIn('open location "https://python.org"', script) | |
| def test_reject_dash_prefixes(self): | |
| with self.assertRaisesRegex( | |
| ValueError, | |
| r"^Invalid URL \(leading dash disallowed\): '--key=val http.*'$" | |
| ): | |
| self.browser.open(f"--key=val {URL}") | |
| class BrowserRegistrationTest(unittest.TestCase): | |
| def setUp(self): | |
| # Ensure we don't alter the real registered browser details | |
| self._saved_tryorder = webbrowser._tryorder | |
| webbrowser._tryorder = [] | |
| self._saved_browsers = webbrowser._browsers | |
| webbrowser._browsers = {} | |
| def tearDown(self): | |
| webbrowser._tryorder = self._saved_tryorder | |
| webbrowser._browsers = self._saved_browsers | |
| def _check_registration(self, preferred): | |
| class ExampleBrowser: | |
| pass | |
| expected_tryorder = [] | |
| expected_browsers = {} | |
| self.assertEqual(webbrowser._tryorder, expected_tryorder) | |
| self.assertEqual(webbrowser._browsers, expected_browsers) | |
| webbrowser.register('Example1', ExampleBrowser) | |
| expected_tryorder = ['Example1'] | |
| expected_browsers['example1'] = [ExampleBrowser, None] | |
| self.assertEqual(webbrowser._tryorder, expected_tryorder) | |
| self.assertEqual(webbrowser._browsers, expected_browsers) | |
| instance = ExampleBrowser() | |
| if preferred is not None: | |
| webbrowser.register('example2', ExampleBrowser, instance, | |
| preferred=preferred) | |
| else: | |
| webbrowser.register('example2', ExampleBrowser, instance) | |
| if preferred: | |
| expected_tryorder = ['example2', 'Example1'] | |
| else: | |
| expected_tryorder = ['Example1', 'example2'] | |
| expected_browsers['example2'] = [ExampleBrowser, instance] | |
| self.assertEqual(webbrowser._tryorder, expected_tryorder) | |
| self.assertEqual(webbrowser._browsers, expected_browsers) | |
| def test_register(self): | |
| self._check_registration(preferred=False) | |
| def test_register_default(self): | |
| self._check_registration(preferred=None) | |
| 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): | |
| webbrowser = import_helper.import_fresh_module('webbrowser') | |
| self.assertIsNone(webbrowser._tryorder) | |
| self.assertFalse(webbrowser._browsers) | |
| class ExampleBrowser: | |
| pass | |
| webbrowser.register('Example1', ExampleBrowser) | |
| self.assertTrue(webbrowser._tryorder) | |
| self.assertEqual(webbrowser._tryorder[-1], 'Example1') | |
| self.assertTrue(webbrowser._browsers) | |
| self.assertIn('example1', webbrowser._browsers) | |
| self.assertEqual(webbrowser._browsers['example1'], [ExampleBrowser, None]) | |
| def test_get(self): | |
| webbrowser = import_helper.import_fresh_module('webbrowser') | |
| self.assertIsNone(webbrowser._tryorder) | |
| self.assertFalse(webbrowser._browsers) | |
| with self.assertRaises(webbrowser.Error): | |
| 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 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, IndexError) as err: | |
| self.skipTest(str(err)) | |
| with os_helper.EnvironmentVarGuard() as env: | |
| env["BROWSER"] = least_preferred_browser | |
| webbrowser = import_helper.import_fresh_module('webbrowser') | |
| self.assertEqual(webbrowser.get().name, least_preferred_browser) | |
| with os_helper.EnvironmentVarGuard() as env: | |
| env["BROWSER"] = sys.executable | |
| webbrowser = import_helper.import_fresh_module('webbrowser') | |
| self.assertEqual(webbrowser.get().name, sys.executable) | |
| @force_not_colorized_test_class | |
| 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() |