Skip to content

gh-128540: launch default browser more often on Linux #130541

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 12 additions & 1 deletion Doc/library/webbrowser.rst
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,11 @@ The following functions are defined:

Returns ``True`` if a browser was successfully launched, ``False`` otherwise.

Note that on some platforms, trying to open a filename using this function,
Note that on some platforms, trying to open a filename (``'./path.html'``) using this function,
may work and start the operating system's associated program. However, this
is neither supported nor portable.
``'file://...'`` URLs, on the other hand, should work and consistently
launch a browser.

.. audit-event:: webbrowser.open url webbrowser.open

Expand Down Expand Up @@ -201,6 +203,15 @@ Notes:
.. versionchanged:: 3.13
Support for iOS has been added.

.. versionadded:: next
Support for launching the XDG default browser via ``gtk-launch`` or ``gio launch`` on POSIX systems,
``exo-open`` in XFCE environments,
and ``kioclient exec`` in KDE environments.

.. versionchanged:: next
``file://`` URLs should now open more reliably in browsers on all platforms,
instead of opening the default application associated with the file type.

Here are some simple examples::

url = 'https://docs.python.org/'
Expand Down
38 changes: 38 additions & 0 deletions Lib/test/test_webbrowser.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,25 @@ def test_open(self):
options=[],
arguments=[URL])

def test_supports_file(self):
self._test('open',
args=["file:///tmp/file"],
options=[],
arguments=["file:///tmp/file"])

def test_not_supports_file(self):
popen = PopenMock()
support.patch(self, subprocess, 'Popen', popen)
browser = self.browser_class("open")
browser._supports_file = False
assert not browser.open("file:///some/file")
assert subprocess.Popen.call_count == 0
url = "https://some-url"
browser.open(url)
assert subprocess.Popen.call_count == 1
popen_args = subprocess.Popen.call_args[0][0]
self.assertEqual(popen_args, ["open", url])


class BackgroundBrowserCommandTest(CommandTestMixin, unittest.TestCase):

Expand All @@ -75,6 +94,25 @@ def test_open(self):
options=[],
arguments=[URL])

def test_supports_file(self):
self._test('open',
args=["file:///tmp/file"],
options=[],
arguments=["file:///tmp/file"])

def test_not_supports_file(self):
popen = PopenMock()
support.patch(self, subprocess, 'Popen', popen)
browser = self.browser_class("open")
browser._supports_file = False
assert not browser.open("file:///some/file")
assert subprocess.Popen.call_count == 0
url = "https://some-url"
browser.open(url)
assert subprocess.Popen.call_count == 1
popen_args = subprocess.Popen.call_args[0][0]
self.assertEqual(popen_args, ["open", url])


class ChromeCommandTest(CommandTestMixin, unittest.TestCase):

Expand Down
93 changes: 84 additions & 9 deletions Lib/webbrowser.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ class GenericBrowser(BaseBrowser):
"""Class for all browsers started with a command
and without remote functionality."""

def __init__(self, name):
def __init__(self, name, *, _supports_file=True):
if isinstance(name, str):
self.name = name
self.args = ["%s"]
Expand All @@ -177,9 +177,20 @@ def __init__(self, name):
self.name = name[0]
self.args = name[1:]
self.basename = os.path.basename(self.name)
# whether it supports file:// URLs
# set to False for generic openers like xdg-open,
# which do not launch webbrowsers reliably
self._supports_file = _supports_file

def open(self, url, new=0, autoraise=True):
sys.audit("webbrowser.open", url)

if not self._supports_file:
# skip me for `file://` URLs for generic openers (e.g. xdg-open)
proto, _sep, _rest = url.partition(":")
if _sep and proto.lower() == "file":
return False

cmdline = [self.name] + [arg.replace("%s", url)
for arg in self.args]
try:
Expand All @@ -197,6 +208,12 @@ class BackgroundBrowser(GenericBrowser):
background."""

def open(self, url, new=0, autoraise=True):
if not self._supports_file:
# skip me for `file://` URLs for generic openers (e.g. xdg-open)
proto, _sep, _rest = url.partition(":")
if _sep and proto.lower() == "file":
return False

cmdline = [self.name] + [arg.replace("%s", url)
for arg in self.args]
sys.audit("webbrowser.open", url)
Expand Down Expand Up @@ -415,33 +432,91 @@ class Edge(UnixBrowser):
# Platform support for Unix
#


def _locate_xdg_desktop(name: str) -> str | None:
"""Locate .desktop file by name

Returns absolute path to .desktop file found on $XDG_DATA search path
or None if no matching .desktop file is found.

Needed for `gio launch` support.
"""
if not name.endswith(".desktop"):
# ensure it ends in .desktop
name += ".desktop"
xdg_data_home = os.environ.get("XDG_DATA_HOME") or os.path.expanduser(
"~/.local/share"
)
xdg_data_dirs = os.environ.get("XDG_DATA_DIRS") or "/usr/local/share/:/usr/share/"
all_data_dirs = [xdg_data_home]
all_data_dirs.extend(xdg_data_dirs.split(os.pathsep))
for data_dir in all_data_dirs:
desktop_path = os.path.join(data_dir, "applications", name)
if os.path.exists(desktop_path):
return desktop_path
return None

# These are the right tests because all these Unix browsers require either
# a console terminal or an X display to run.

def register_X_browsers():

# use gtk[4]-launch to launch preferred browser by name, if found
# this should be _before_ xdg-open, which doesn't necessarily launch a browser
if _os_preferred_browser:
for gtk_launch in ("gtk4-launch", "gtk-launch"):
if shutil.which(gtk_launch):
register(
gtk_launch,
None,
BackgroundBrowser([gtk_launch, _os_preferred_browser, "%s"]),
)

# use xdg-open if around
if shutil.which("xdg-open"):
register("xdg-open", None, BackgroundBrowser("xdg-open"))
# `xdg-open` does NOT guarantee a browser is launched,
# so skip it for `file://`
register("xdg-open", None, BackgroundBrowser("xdg-open", _supports_file=False))


# Opens an appropriate browser for the URL scheme according to
# Opens the default application for the URL scheme according to
# freedesktop.org settings (GNOME, KDE, XFCE, etc.)
if shutil.which("gio"):
register("gio", None, BackgroundBrowser(["gio", "open", "--", "%s"]))
if _os_preferred_browser:
absolute_browser = _locate_xdg_desktop(_os_preferred_browser)
if absolute_browser:
register(
"gio-launch",
None,
BackgroundBrowser(["gio", "launch", absolute_browser, "%s"]),
)
# `gio open` does NOT guarantee a browser is launched
register("gio", None, BackgroundBrowser(["gio", "open", "--", "%s"], _supports_file=False))

xdg_desktop = os.getenv("XDG_CURRENT_DESKTOP", "").split(":")

# The default GNOME3 browser
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"))
register("gvfs-open", None, BackgroundBrowser("gvfs-open", _supports_file=False))

# The default KDE browser
if (("KDE" in xdg_desktop or
"KDE_FULL_SESSION" in os.environ) and
shutil.which("kfmclient")):
register("kfmclient", Konqueror, Konqueror("kfmclient"))
if ("KDE" in xdg_desktop or
"KDE_FULL_SESSION" in os.environ):
if shutil.which("kioclient"):
# launch URL with http[s] handler
register("kioclient", None, BackgroundBrowser(["kioclient", "exec", "%s", "x-scheme-handler/https"]))
if shutil.which("kfmclient"):
register("kfmclient", Konqueror, Konqueror("kfmclient"))

# The default XFCE browser
if "XFCE" in xdg_desktop and shutil.which("exo-open"):
register(
"exo-open",
None,
BackgroundBrowser(["exo-open", "--launch", "WebBrowser", "%s"]),
)

# Common symbolic link for the default X11 browser
if shutil.which("x-www-browser"):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
:func:`webbrowser.open` should launch default webbrowsers for URLs that are
not ``http[s]://`` more often (especially ``file://``,
where the default application by file type was often launched, instead of a browser).
This works by adding support for ``gtk-launch`` and ``gio
launch``,
and making sure generic application launchers like ``xdg-open`` are not used for ``file://`` URLs.
Loading