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".
This commit is contained in:
Eric S. Raymond 2001-01-23 13:16:32 +00:00
parent f87d857080
commit f7f185116a
2 changed files with 245 additions and 180 deletions

View file

@ -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 display isn't available. If text-mode browsers are used, the calling
process will block until the user exits the browser. 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 For non-\UNIX{} platforms, or when X11 browsers are available on
\UNIX, the controlling process will not wait for the user to finish \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 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} \begin{funcdesc}{open_new}{url}
Open \var{url} in a new window of the default browser, if possible, 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} \end{funcdesc}
\begin{funcdesc}{get}{\optional{name}} \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} \end{funcdesc}
\begin{funcdesc}{register}{name, constructor\optional{, instance}} \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 \code{None}, \var{constructor} will be called without parameters to
create an instance when needed. If \var{instance} is provided, create an instance when needed. If \var{instance} is provided,
\var{constructor} will never be called, and may be \code{None}. \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} \end{funcdesc}
Several browser types are defined. This table gives the type names 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. the implementation classes, all defined in this module.
\begin{tableiii}{l|l|c}{code}{Type Name}{Class Name}{Notes} \begin{tableiii}{l|l|c}{code}{Type Name}{Class Name}{Notes}
\lineiii{'mozilla'}{\class{Mozilla}}{}
\lineiii{'netscape'}{\class{Netscape}}{} \lineiii{'netscape'}{\class{Netscape}}{}
\lineiii{'mosaic'}{\class{Mosaic}}{}
\lineiii{'kfm'}{\class{Konquerer}}{(1)} \lineiii{'kfm'}{\class{Konquerer}}{(1)}
\lineiii{'grail'}{\class{Grail}}{} \lineiii{'grail'}{\class{Grail}}{}
\lineiii{'links'}{\class{links}}{}
\lineiii{'lynx'}{\class{Lynx}}{}
\lineiii{'w3m'}{\class{w3m}}{}
\lineiii{'windows-default'}{\class{WindowsDefault}}{(2)} \lineiii{'windows-default'}{\class{WindowsDefault}}{(2)}
\lineiii{'internet-config'}{\class{InternetConfig}}{(3)} \lineiii{'internet-config'}{\class{InternetConfig}}{(3)}
\lineiii{'command-line'}{\class{CommandLineBrowser}}{}
\end{tableiii} \end{tableiii}
\noindent \noindent
@ -98,5 +118,6 @@ module-level convenience functions:
\begin{funcdesc}{open_new}{url} \begin{funcdesc}{open_new}{url}
Open \var{url} in a new window of the browser handled by this Open \var{url} in a new window of the browser handled by this
controller, if possible, otherwise, open \var{url} in the only 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} \end{funcdesc}

View file

@ -1,47 +1,66 @@
"""Remote-control interfaces to some browsers.""" """Remote-control interfaces to common browsers."""
import os import os
import sys import sys
PROCESS_CREATION_DELAY = 4
class Error(Exception): class Error(Exception):
pass pass
_browsers = {} # Dictionary of available browser controllers
_browsers = {} _tryorder = [] # Preference order of available browsers
def register(name, klass, instance=None): def register(name, klass, instance=None):
"""Register a browser connector and, optionally, connection.""" """Register a browser connector and, optionally, connection."""
_browsers[name.lower()] = [klass, instance] _browsers[name.lower()] = [klass, instance]
def get(using=None):
def get(name=None): """Return a browser launcher instance appropriate for the environment."""
"""Retrieve a connection to a browser by type name, or the default if using:
browser.""" alternatives = [using]
name = name or DEFAULT_BROWSER else:
try: alternatives = _tryorder
L = _browsers[name.lower()] for browser in alternatives:
except KeyError: if browser.find('%s') > -1:
raise ValueError, "unknown browser type: " + `name` # User gave us a command line, don't mess with it.
if L[1] is None: return browser
L[1] = L[0]() else:
return L[1] # 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. # Please note: the following definition hides a builtin function.
def open(url, new=0): def open(url, new=0):
get().open(url, new) 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): # 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")
def _iscommand(cmd):
"""Return true if cmd can be found on the executable search path.""" """Return true if cmd can be found on the executable search path."""
path = os.environ.get("PATH") path = os.environ.get("PATH")
if not path: if not path:
@ -52,59 +71,72 @@ def _iscommand(cmd):
return 1 return 1
return 0 return 0
class GenericBrowser:
class CommandLineBrowser: def __init__(self, cmd):
_browsers = [] self.command = cmd
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): def open(self, url, new=0):
for exe, cmd in self._browsers: os.system(self.command % url)
if _iscommand(exe):
os.system(cmd % url)
return
raise Error("could not locate runnable browser")
def open_new(self, url): def open_new(self, url): # Deprecated. May be removed in 2.1.
self.open(url) self.open(url)
register("command-line", CommandLineBrowser) # Easy cases first -- register console browsers if we have them.
if os.environ.get("TERM"):
# The Links browser <http://artax.karlin.mff.cuni.cz/~mikulas/links/>
if _iscommand("links"):
register("links", None, GenericBrowser("links %s"))
# The Lynx browser <http://lynx.browser.org/>
if _iscommand("lynx"):
register("lynx", None, GenericBrowser("lynx %s"))
# The w3m browser <http://ei5nazha.yz.yamagata-u.ac.jp/~aito/w3m/eng/>
if _iscommand("w3m"):
register("w3m", None, GenericBrowser("w3m %s"))
# X browsers have mre in the way of options
class Netscape: if os.environ.get("DISPLAY"):
# First, the Netscape series
if _iscommand("netscape") or _iscommand("mozilla"):
class Netscape:
"Launcher class for Netscape browsers."
autoRaise = 1 autoRaise = 1
def __init__(self, name):
self.name = name
def _remote(self, action): def _remote(self, action):
raise_opt = ("-noraise", "-raise")[self.autoRaise] raise_opt = ("-noraise", "-raise")[self.autoRaise]
cmd = "netscape %s -remote '%s' >/dev/null 2>&1" % (raise_opt, action) cmd = "%s %s -remote '%s' >/dev/null 2>&1" % (self.name, raise_opt, action)
rc = os.system(cmd) rc = os.system(cmd)
if rc: if rc:
import time import time
os.system("netscape -no-about-splash &") os.system("%s -no-about-splash &" % self.name)
time.sleep(PROCESS_CREATION_DELAY) time.sleep(PROCESS_CREATION_DELAY)
rc = os.system(cmd) rc = os.system(cmd)
return not rc return not rc
def open(self, url, new=0): def open(self, url, new=0):
if new: if new:
self.open_new(url) self._remote("openURL(%s, new-window)" % url)
else: else:
self._remote("openURL(%s)" % url) self._remote("openURL(%s)" % url)
# Deprecated. May be removed in 2.1.
def open_new(self, url): def open_new(self, url):
self._remote("openURL(%s, new-window)" % url) self.open(url, 1)
register("netscape", Netscape) 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 &"))
class Konqueror: # Konqueror/kfm, the KDE browser.
if _iscommand("kfm"):
class Konqueror:
"""Controller for the KDE File Manager (kfm, or Konqueror). """Controller for the KDE File Manager (kfm, or Konqueror).
See http://developer.kde.org/documentation/other/kfmclient.html See http://developer.kde.org/documentation/other/kfmclient.html
@ -122,20 +154,21 @@ class Konqueror:
return not rc return not rc
def open(self, url, new=1): def open(self, url, new=1):
# XXX currently I know no way to prevent KFM from opening a new win. # 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) self._remote("openURL %s" % url)
register("kfm", Konqueror) # Deprecated. May be removed in 2.1.
open_new = open
class Grail: register("kfm", Konqueror, None)
# 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!
# 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): def _find_grail_rc(self):
import glob import glob
import pwd import pwd
@ -171,42 +204,36 @@ class Grail:
def open(self, url, new=0): def open(self, url, new=0):
if new: if new:
self.open_new(url) self._remote("LOADNEW " + url)
else: else:
self._remote("LOAD " + url) self._remote("LOAD " + url)
# Deprecated. May be removed in 2.1.
def open_new(self, url): def open_new(self, url):
self._remote("LOADNEW " + url) self.open(url, 1)
register("grail", Grail) register("grail", Grail, None)
#
# Platform support for Windows
#
class WindowsDefault: if sys.platform[:3] == "win":
global _tryorder
_tryorder = ("netscape", "windows-default")
class WindowsDefault:
def open(self, url, new=0): def open(self, url, new=0):
os.startfile(url) os.startfile(url)
def open_new(self, url): def open_new(self, url): # Deprecated. May be removed in 2.1.
self.open(url) self.open(url)
DEFAULT_BROWSER = "command-line"
if sys.platform[:3] == "win":
del _browsers["kfm"]
register("windows-default", WindowsDefault) 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 # Platform support for MacOS
#
# Now try to support the MacOS world. This is the only supported
# controller on that platform, so don't mess with the default!
try: try:
import ic import ic
@ -217,9 +244,26 @@ else:
def open(self, url, new=0): def open(self, url, new=0):
ic.launchurl(url) ic.launchurl(url)
def open_new(self, url): def open_new(self, url): # Deprecated. May be removed in 2.1.
self.open(url) 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) 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