diff --git a/Lib/test/test_webbrowser.py b/Lib/test/test_webbrowser.py index 4fcbc5c2e59ea3..68674a36a03693 100644 --- a/Lib/test/test_webbrowser.py +++ b/Lib/test/test_webbrowser.py @@ -301,6 +301,44 @@ def test_open_new_tab(self): self._test('open_new_tab') +@unittest.skipUnless(sys.platform[:3] == "win", "Windows test") +class WindowsDefaultTest(unittest.TestCase): + def setUp(self): + support.patch(self, os, "startfile", mock.Mock()) + self.browser = webbrowser.WindowsDefault() + support.patch(self, self.browser, "_open_default_browser", mock.Mock()) + + def test_default(self): + browser = webbrowser.get() + self.assertIsInstance(browser, webbrowser.WindowsDefault) + + def test_open_startfile(self): + url = "https://python.org" + self.browser.open(url) + self.browser._open_default_browser.assert_not_called() + os.startfile.assert_called_with(url) + + def test_open_browser_lookup(self): + url = "file://python.org" + self.browser.open(url) + self.browser._open_default_browser.assert_called_with(url) + os.startfile.assert_not_called() + + def test_open_browser_lookup_fails(self): + url = "file://python.org" + self.browser._open_default_browser.return_value = False + self.browser.open(url) + self.browser._open_default_browser.assert_called_with(url) + os.startfile.assert_called_with(url) + + def test_open_browser_lookup_error(self): + url = "file://python.org" + self.browser._open_default_browser.side_effect = OSError('registry failed...') + self.browser.open(url) + self.browser._open_default_browser.assert_called_with(url) + os.startfile.assert_called_with(url) + + class BrowserRegistrationTest(unittest.TestCase): def setUp(self): diff --git a/Lib/webbrowser.py b/Lib/webbrowser.py index d2efc72113a917..0fc54880c03d66 100644 --- a/Lib/webbrowser.py +++ b/Lib/webbrowser.py @@ -573,8 +573,61 @@ def register_standard_browsers(): if sys.platform[:3] == "win": class WindowsDefault(BaseBrowser): + def _open_default_browser(self, url): + """Open a URL with the default browser + + launches the web browser no matter what `url` is, unlike startfile. + + Raises OSError if registry lookups fail. + Returns False if URL not opened. + """ + try: + import winreg + except ImportError: + return False + # lookup progId for https URLs + # e.g. 'FirefoxURL-abc123' + with winreg.OpenKey( + winreg.HKEY_CURRENT_USER, + r"Software\Microsoft\Windows\Shell\Associations\UrlAssociations\https\UserChoice", + ) as key: + browser_id = winreg.QueryValueEx(key, "ProgId")[0] + # lookup launch command-line + # e.g. '"C:\\Program Files\\Mozilla Firefox\\firefox.exe" -osint -url "%1"' + with winreg.OpenKey( + winreg.HKEY_CLASSES_ROOT, + rf"{browser_id}\shell\open\command", + ) as key: + browser_cmd = winreg.QueryValueEx(key, "")[0] + + # build command-line + if "%1" not in browser_cmd: + # Command is missing '%1' placeholder, + # so we don't know how to build the command to open a file + # would append be safe in this case? + return False + + # the rest copied from BackgroundBrowser + cmdline = [arg.replace("%1", url) for arg in shlex.split(browser_cmd)] + try: + p = subprocess.Popen(cmdline) + return p.poll() is None + except OSError: + return False + def open(self, url, new=0, autoraise=True): sys.audit("webbrowser.open", url) + proto, _sep, _rest = url.partition(":") + if _sep and proto.lower() not in {"http", "https"}: + # need to lookup browser if it's not a web URL + try: + opened = self._open_default_browser(url) + except OSError: + # failed to lookup registry items + opened = False + if opened: + return opened + try: os.startfile(url) except OSError: diff --git a/Misc/NEWS.d/next/Windows/2025-02-25-11-38-45.gh-issue-128540.nTM0bU.rst b/Misc/NEWS.d/next/Windows/2025-02-25-11-38-45.gh-issue-128540.nTM0bU.rst new file mode 100644 index 00000000000000..ab3b0d8fab2a7d --- /dev/null +++ b/Misc/NEWS.d/next/Windows/2025-02-25-11-38-45.gh-issue-128540.nTM0bU.rst @@ -0,0 +1,2 @@ +Ensure web browser is launched by :func:`webbrowser.open` on Windows, even +for ``file://`` URLs.