From f7f185116a8274b105edc1be64ffc9c8061c7f43 Mon Sep 17 00:00:00 2001 From: "Eric S. Raymond" Date: Tue, 23 Jan 2001 13:16:32 +0000 Subject: [PATCH] Rewrite webbrowser.py to fix various bugs following Ka-Ping Yee's complaints. The new version moves most of its initialization to package load time; it's simpler, faster, smaller, and adds support for Mozilla and Links. Interpretation of the BROWSER variable now works and is documented. The open_new entry point and methods are marked "deprecated; may be removed in 2.1". --- Doc/lib/libwebbrowser.tex | 29 ++- Lib/webbrowser.py | 396 +++++++++++++++++++++----------------- 2 files changed, 245 insertions(+), 180 deletions(-) diff --git a/Doc/lib/libwebbrowser.tex b/Doc/lib/libwebbrowser.tex index 725afa7cdea..113bef390dd 100644 --- a/Doc/lib/libwebbrowser.tex +++ b/Doc/lib/libwebbrowser.tex @@ -15,6 +15,15 @@ browsers will be used if graphical browsers are not available or an X11 display isn't available. If text-mode browsers are used, the calling process will block until the user exits the browser. +Under \UNIX, if the environment variable \envvar{BROWSER} exists, it +is interpreted to override the platform default browser, as a +colon-separated list of browsers to try in order. When the value of +a list part contains the string \code{\%s}, then it is interpreted as +a literal browser command line to be used with the argument URL +substituted for the \code{\%s}; if the part does not contain, +\code{\%s}, it is simply interpreted as the name of the browser to +launch. + For non-\UNIX{} platforms, or when X11 browsers are available on \UNIX, the controlling process will not wait for the user to finish with the browser, but allow the browser to maintain its own window on @@ -35,11 +44,14 @@ The following functions are defined: \begin{funcdesc}{open_new}{url} Open \var{url} in a new window of the default browser, if possible, - otherwise, open \var{url} in the only browser window. + otherwise, open \var{url} in the only browser window. (This entry + point is deprecated and may be removed in 2.1.) \end{funcdesc} \begin{funcdesc}{get}{\optional{name}} - Return a controller object for the browser type \var{name}. + Return a controller object for the browser type \var{name}. If + \var{name} is empty, return a controller for a default browser + appriopriate \end{funcdesc} \begin{funcdesc}{register}{name, constructor\optional{, instance}} @@ -49,6 +61,10 @@ The following functions are defined: \code{None}, \var{constructor} will be called without parameters to create an instance when needed. If \var{instance} is provided, \var{constructor} will never be called, and may be \code{None}. + + This entry point is only useful if you plan to either set the + \envvar{BROWSER} variable or call \function{get} with a nonempty + argument matching the name of a handler you declare. \end{funcdesc} Several browser types are defined. This table gives the type names @@ -56,12 +72,16 @@ that may be passed to the \function{get()} function and the names of the implementation classes, all defined in this module. \begin{tableiii}{l|l|c}{code}{Type Name}{Class Name}{Notes} + \lineiii{'mozilla'}{\class{Mozilla}}{} \lineiii{'netscape'}{\class{Netscape}}{} + \lineiii{'mosaic'}{\class{Mosaic}}{} \lineiii{'kfm'}{\class{Konquerer}}{(1)} \lineiii{'grail'}{\class{Grail}}{} + \lineiii{'links'}{\class{links}}{} + \lineiii{'lynx'}{\class{Lynx}}{} + \lineiii{'w3m'}{\class{w3m}}{} \lineiii{'windows-default'}{\class{WindowsDefault}}{(2)} \lineiii{'internet-config'}{\class{InternetConfig}}{(3)} - \lineiii{'command-line'}{\class{CommandLineBrowser}}{} \end{tableiii} \noindent @@ -98,5 +118,6 @@ module-level convenience functions: \begin{funcdesc}{open_new}{url} Open \var{url} in a new window of the browser handled by this controller, if possible, otherwise, open \var{url} in the only - browser window. + browser window. (This method is deprecated and may be removed in + 2.1.) \end{funcdesc} diff --git a/Lib/webbrowser.py b/Lib/webbrowser.py index 2fa11487938..36dfdc7e145 100644 --- a/Lib/webbrowser.py +++ b/Lib/webbrowser.py @@ -1,212 +1,239 @@ -"""Remote-control interfaces to some browsers.""" +"""Remote-control interfaces to common browsers.""" import os import sys - -PROCESS_CREATION_DELAY = 4 - - class Error(Exception): pass - -_browsers = {} +_browsers = {} # Dictionary of available browser controllers +_tryorder = [] # Preference order of available browsers def register(name, klass, instance=None): """Register a browser connector and, optionally, connection.""" _browsers[name.lower()] = [klass, instance] - -def get(name=None): - """Retrieve a connection to a browser by type name, or the default - browser.""" - name = name or DEFAULT_BROWSER - try: - L = _browsers[name.lower()] - except KeyError: - raise ValueError, "unknown browser type: " + `name` - if L[1] is None: - L[1] = L[0]() - return L[1] - +def get(using=None): + """Return a browser launcher instance appropriate for the environment.""" + if using: + alternatives = [using] + else: + alternatives = _tryorder + for browser in alternatives: + if browser.find('%s') > -1: + # User gave us a command line, don't mess with it. + return browser + else: + # User gave us a browser name. + command = _browsers[browser.lower()] + if command[1] is None: + return command[0]() + else: + return command[1] + raise Error("could not locate runnable browser") # Please note: the following definition hides a builtin function. def open(url, new=0): get().open(url, new) +def open_new(url): # Marked deprecated. May be removed in 2.1. + get().open(url, 1) -def open_new(url): - get().open_new(url) +# +# Everything after this point initializes _browsers and _tryorder, +# then disappears. Some class definitions and instances remain +# live through these globals, but only the minimum set needed to +# support the user's platform. +# +# +# Platform support for Unix +# -def _iscommand(cmd): - """Return true if cmd can be found on the executable search path.""" - path = os.environ.get("PATH") - if not path: - return 0 - for d in path.split(os.pathsep): - exe = os.path.join(d, cmd) - if os.path.isfile(exe): - return 1 - return 0 +# This is the right test because all these Unix browsers require either +# a console terminal of an X display to run. Note that we cannot split +# the TERM and DISPLAY cases, because we might be running Python from inside +# an xterm. +if os.environ.get("TERM") or os.environ.get("DISPLAY"): + PROCESS_CREATION_DELAY = 4 + global tryorder + _tryorder = ("mozilla","netscape","kfm","grail","links","lynx","w3m") - -class CommandLineBrowser: - _browsers = [] - if os.environ.get("DISPLAY"): - _browsers.extend([ - ("netscape", "netscape %s >/dev/null &"), - ("mosaic", "mosaic %s >/dev/null &"), - ]) - _browsers.extend([ - ("lynx", "lynx %s"), - ("w3m", "w3m %s"), - ]) - - def open(self, url, new=0): - for exe, cmd in self._browsers: - if _iscommand(exe): - os.system(cmd % url) - return - raise Error("could not locate runnable browser") - - def open_new(self, url): - self.open(url) - -register("command-line", CommandLineBrowser) - - -class Netscape: - autoRaise = 1 - - def _remote(self, action): - raise_opt = ("-noraise", "-raise")[self.autoRaise] - cmd = "netscape %s -remote '%s' >/dev/null 2>&1" % (raise_opt, action) - rc = os.system(cmd) - if rc: - import time - os.system("netscape -no-about-splash &") - time.sleep(PROCESS_CREATION_DELAY) - rc = os.system(cmd) - return not rc - - def open(self, url, new=0): - if new: - self.open_new(url) - else: - self._remote("openURL(%s)" % url) - - def open_new(self, url): - self._remote("openURL(%s, new-window)" % url) - -register("netscape", Netscape) - - -class Konqueror: - """Controller for the KDE File Manager (kfm, or Konqueror). - - See http://developer.kde.org/documentation/other/kfmclient.html - for more information on the Konqueror remote-control interface. - - """ - def _remote(self, action): - cmd = "kfmclient %s >/dev/null 2>&1" % action - rc = os.system(cmd) - if rc: - import time - os.system("kfm -d &") - time.sleep(PROCESS_CREATION_DELAY) - rc = os.system(cmd) - return not rc - - def open(self, url, new=1): - # XXX currently I know no way to prevent KFM from opening a new win. - self.open_new(url) - - def open_new(self, url): - self._remote("openURL %s" % url) - -register("kfm", Konqueror) - - -class Grail: - # 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(tempdir, 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 socket.error: - # no good; attempt to clean it out, but don't fail: - try: - os.unlink(fn) - except IOError: - pass - else: - return s - - def _remote(self, action): - s = self._find_grail_rc() - if not s: + def _iscommand(cmd): + """Return true if cmd can be found on the executable search path.""" + path = os.environ.get("PATH") + if not path: return 0 - s.send(action) - s.close() - return 1 + for d in path.split(os.pathsep): + exe = os.path.join(d, cmd) + if os.path.isfile(exe): + return 1 + return 0 - def open(self, url, new=0): - if new: - self.open_new(url) - else: - self._remote("LOAD " + url) + class GenericBrowser: + def __init__(self, cmd): + self.command = cmd - def open_new(self, url): - self._remote("LOADNEW " + url) + def open(self, url, new=0): + os.system(self.command % url) -register("grail", Grail) + def open_new(self, url): # Deprecated. May be removed in 2.1. + self.open(url) + # Easy cases first -- register console browsers if we have them. + if os.environ.get("TERM"): + # The Links browser + if _iscommand("links"): + register("links", None, GenericBrowser("links %s")) + # The Lynx browser + if _iscommand("lynx"): + register("lynx", None, GenericBrowser("lynx %s")) + # The w3m browser + if _iscommand("w3m"): + register("w3m", None, GenericBrowser("w3m %s")) -class WindowsDefault: - def open(self, url, new=0): - os.startfile(url) + # X browsers have mre in the way of options + if os.environ.get("DISPLAY"): + # First, the Netscape series + if _iscommand("netscape") or _iscommand("mozilla"): + class Netscape: + "Launcher class for Netscape browsers." + autoRaise = 1 - def open_new(self, url): - self.open(url) + def __init__(self, name): + self.name = name + def _remote(self, action): + raise_opt = ("-noraise", "-raise")[self.autoRaise] + cmd = "%s %s -remote '%s' >/dev/null 2>&1" % (self.name, raise_opt, action) + rc = os.system(cmd) + if rc: + import time + os.system("%s -no-about-splash &" % self.name) + time.sleep(PROCESS_CREATION_DELAY) + rc = os.system(cmd) + return not rc -DEFAULT_BROWSER = "command-line" + def open(self, url, new=0): + if new: + self._remote("openURL(%s, new-window)" % url) + else: + self._remote("openURL(%s)" % url) + + # Deprecated. May be removed in 2.1. + def open_new(self, url): + self.open(url, 1) + + if _iscommand("mozilla"): + register("mozilla", None, Netscape("mozilla")) + if _iscommand("netscape"): + register("netscape", None, Netscape("netscape")) + + # Next, Mosaic -- old but still in use. + if _iscommand("mosaic"): + register("mosaic", None, GenericBrowser("mosaic %s >/dev/null &")) + + # Konqueror/kfm, the KDE browser. + if _iscommand("kfm"): + class Konqueror: + """Controller for the KDE File Manager (kfm, or Konqueror). + + See http://developer.kde.org/documentation/other/kfmclient.html + for more information on the Konqueror remote-control interface. + + """ + def _remote(self, action): + cmd = "kfmclient %s >/dev/null 2>&1" % action + rc = os.system(cmd) + if rc: + import time + os.system("kfm -d &") + time.sleep(PROCESS_CREATION_DELAY) + rc = os.system(cmd) + return not rc + + def open(self, url, new=1): + # XXX Currently I know no way to prevent KFM from opening a new win. + self._remote("openURL %s" % url) + + # Deprecated. May be removed in 2.1. + open_new = open + + + register("kfm", Konqueror, None) + + # Grail, the Python browser. + if _iscommand("grail"): + class Grail: + # 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 neverwill! + 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(tempdir, 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 socket.error: + # no good; attempt to clean it out, but don't fail: + try: + os.unlink(fn) + except IOError: + pass + else: + return s + + 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): + if new: + self._remote("LOADNEW " + url) + else: + self._remote("LOAD " + url) + + # Deprecated. May be removed in 2.1. + def open_new(self, url): + self.open(url, 1) + + register("grail", Grail, None) + +# +# Platform support for Windows +# if sys.platform[:3] == "win": - del _browsers["kfm"] + global _tryorder + _tryorder = ("netscape", "windows-default") + + class WindowsDefault: + def open(self, url, new=0): + os.startfile(url) + + def open_new(self, url): # Deprecated. May be removed in 2.1. + self.open(url) + register("windows-default", WindowsDefault) - DEFAULT_BROWSER = "windows-default" -elif os.environ.get("DISPLAY"): - if _iscommand("netscape"): - DEFAULT_BROWSER = "netscape" -# If the $BROWSER environment variable is set and true, let that be -# the name of the browser to use: # -DEFAULT_BROWSER = os.environ.get("BROWSER") or DEFAULT_BROWSER - - -# Now try to support the MacOS world. This is the only supported -# controller on that platform, so don't mess with the default! +# Platform support for MacOS +# try: import ic @@ -217,9 +244,26 @@ else: def open(self, url, new=0): ic.launchurl(url) - def open_new(self, url): + def open_new(self, url): # Deprecated. May be removed in 2.1. self.open(url) - _browsers.clear() + # internet-config is the only supported controller on MacOS, + # so don't mess with the default! + _tryorder = ("internet-config") register("internet-config", InternetConfig) - DEFAULT_BROWSER = "internet-config" + +# OK, now that we know what the default preference orders for each +# platform are, allow user to override them with the BROWSER variable. +# +if os.environ.has_key("BROWSER"): + # It's the user's responsibility to register handlers for any unknown + # browser referenced by this value, before calling open(). + _tryorder = os.environ["BROWSER"].split(":") +else: + # Optimization: filter out alternatives that aren't available, so we can + # avoid has_key() tests at runtime. (This may also allow some unused + # classes and class-instance storage to be garbage-collected.) + _tryorder = filter(lambda x: _browsers.has_key(x.lower()) or x.find("%s")>-1,\ + _tryorder) + +# end