[Bug #1536021] Mention __hash__ change
[pytest.git] / Lib / webbrowser.py
blob7a1a3b4994c0d1b9b0ada43824328bdb39089d17
1 #! /usr/bin/env python
2 """Interfaces for launching and remotely controlling Web browsers."""
4 import os
5 import sys
6 import stat
7 import subprocess
8 import time
10 __all__ = ["Error", "open", "open_new", "open_new_tab", "get", "register"]
12 class Error(Exception):
13 pass
15 _browsers = {} # Dictionary of available browser controllers
16 _tryorder = [] # Preference order of available browsers
18 def register(name, klass, instance=None, update_tryorder=1):
19 """Register a browser connector and, optionally, connection."""
20 _browsers[name.lower()] = [klass, instance]
21 if update_tryorder > 0:
22 _tryorder.append(name)
23 elif update_tryorder < 0:
24 _tryorder.insert(0, name)
26 def get(using=None):
27 """Return a browser launcher instance appropriate for the environment."""
28 if using is not None:
29 alternatives = [using]
30 else:
31 alternatives = _tryorder
32 for browser in alternatives:
33 if '%s' in browser:
34 # User gave us a command line, split it into name and args
35 return GenericBrowser(browser.split())
36 else:
37 # User gave us a browser name or path.
38 try:
39 command = _browsers[browser.lower()]
40 except KeyError:
41 command = _synthesize(browser)
42 if command[1] is not None:
43 return command[1]
44 elif command[0] is not None:
45 return command[0]()
46 raise Error("could not locate runnable browser")
48 # Please note: the following definition hides a builtin function.
49 # It is recommended one does "import webbrowser" and uses webbrowser.open(url)
50 # instead of "from webbrowser import *".
52 def open(url, new=0, autoraise=1):
53 for name in _tryorder:
54 browser = get(name)
55 if browser.open(url, new, autoraise):
56 return True
57 return False
59 def open_new(url):
60 return open(url, 1)
62 def open_new_tab(url):
63 return open(url, 2)
66 def _synthesize(browser, update_tryorder=1):
67 """Attempt to synthesize a controller base on existing controllers.
69 This is useful to create a controller when a user specifies a path to
70 an entry in the BROWSER environment variable -- we can copy a general
71 controller to operate using a specific installation of the desired
72 browser in this way.
74 If we can't create a controller in this way, or if there is no
75 executable for the requested browser, return [None, None].
77 """
78 cmd = browser.split()[0]
79 if not _iscommand(cmd):
80 return [None, None]
81 name = os.path.basename(cmd)
82 try:
83 command = _browsers[name.lower()]
84 except KeyError:
85 return [None, None]
86 # now attempt to clone to fit the new name:
87 controller = command[1]
88 if controller and name.lower() == controller.basename:
89 import copy
90 controller = copy.copy(controller)
91 controller.name = browser
92 controller.basename = os.path.basename(browser)
93 register(browser, None, controller, update_tryorder)
94 return [None, controller]
95 return [None, None]
98 if sys.platform[:3] == "win":
99 def _isexecutable(cmd):
100 cmd = cmd.lower()
101 if os.path.isfile(cmd) and cmd.endswith((".exe", ".bat")):
102 return True
103 for ext in ".exe", ".bat":
104 if os.path.isfile(cmd + ext):
105 return True
106 return False
107 else:
108 def _isexecutable(cmd):
109 if os.path.isfile(cmd):
110 mode = os.stat(cmd)[stat.ST_MODE]
111 if mode & stat.S_IXUSR or mode & stat.S_IXGRP or mode & stat.S_IXOTH:
112 return True
113 return False
115 def _iscommand(cmd):
116 """Return True if cmd is executable or can be found on the executable
117 search path."""
118 if _isexecutable(cmd):
119 return True
120 path = os.environ.get("PATH")
121 if not path:
122 return False
123 for d in path.split(os.pathsep):
124 exe = os.path.join(d, cmd)
125 if _isexecutable(exe):
126 return True
127 return False
130 # General parent classes
132 class BaseBrowser(object):
133 """Parent class for all browsers. Do not use directly."""
135 args = ['%s']
137 def __init__(self, name=""):
138 self.name = name
139 self.basename = name
141 def open(self, url, new=0, autoraise=1):
142 raise NotImplementedError
144 def open_new(self, url):
145 return self.open(url, 1)
147 def open_new_tab(self, url):
148 return self.open(url, 2)
151 class GenericBrowser(BaseBrowser):
152 """Class for all browsers started with a command
153 and without remote functionality."""
155 def __init__(self, name):
156 if isinstance(name, basestring):
157 self.name = name
158 else:
159 # name should be a list with arguments
160 self.name = name[0]
161 self.args = name[1:]
162 self.basename = os.path.basename(self.name)
164 def open(self, url, new=0, autoraise=1):
165 cmdline = [self.name] + [arg.replace("%s", url)
166 for arg in self.args]
167 try:
168 p = subprocess.Popen(cmdline, close_fds=True)
169 return not p.wait()
170 except OSError:
171 return False
174 class BackgroundBrowser(GenericBrowser):
175 """Class for all browsers which are to be started in the
176 background."""
178 def open(self, url, new=0, autoraise=1):
179 cmdline = [self.name] + [arg.replace("%s", url)
180 for arg in self.args]
181 setsid = getattr(os, 'setsid', None)
182 if not setsid:
183 setsid = getattr(os, 'setpgrp', None)
184 try:
185 p = subprocess.Popen(cmdline, close_fds=True, preexec_fn=setsid)
186 return (p.poll() is None)
187 except OSError:
188 return False
191 class UnixBrowser(BaseBrowser):
192 """Parent class for all Unix browsers with remote functionality."""
194 raise_opts = None
195 remote_args = ['%action', '%s']
196 remote_action = None
197 remote_action_newwin = None
198 remote_action_newtab = None
199 background = False
200 redirect_stdout = True
202 def _invoke(self, args, remote, autoraise):
203 raise_opt = []
204 if remote and self.raise_opts:
205 # use autoraise argument only for remote invocation
206 autoraise = int(bool(autoraise))
207 opt = self.raise_opts[autoraise]
208 if opt: raise_opt = [opt]
210 cmdline = [self.name] + raise_opt + args
212 if remote or self.background:
213 inout = file(os.devnull, "r+")
214 else:
215 # for TTY browsers, we need stdin/out
216 inout = None
217 # if possible, put browser in separate process group, so
218 # keyboard interrupts don't affect browser as well as Python
219 setsid = getattr(os, 'setsid', None)
220 if not setsid:
221 setsid = getattr(os, 'setpgrp', None)
223 p = subprocess.Popen(cmdline, close_fds=True, stdin=inout,
224 stdout=(self.redirect_stdout and inout or None),
225 stderr=inout, preexec_fn=setsid)
226 if remote:
227 # wait five secons. If the subprocess is not finished, the
228 # remote invocation has (hopefully) started a new instance.
229 time.sleep(1)
230 rc = p.poll()
231 if rc is None:
232 time.sleep(4)
233 rc = p.poll()
234 if rc is None:
235 return True
236 # if remote call failed, open() will try direct invocation
237 return not rc
238 elif self.background:
239 if p.poll() is None:
240 return True
241 else:
242 return False
243 else:
244 return not p.wait()
246 def open(self, url, new=0, autoraise=1):
247 if new == 0:
248 action = self.remote_action
249 elif new == 1:
250 action = self.remote_action_newwin
251 elif new == 2:
252 if self.remote_action_newtab is None:
253 action = self.remote_action_newwin
254 else:
255 action = self.remote_action_newtab
256 else:
257 raise Error("Bad 'new' parameter to open(); " +
258 "expected 0, 1, or 2, got %s" % new)
260 args = [arg.replace("%s", url).replace("%action", action)
261 for arg in self.remote_args]
262 success = self._invoke(args, True, autoraise)
263 if not success:
264 # remote invocation failed, try straight way
265 args = [arg.replace("%s", url) for arg in self.args]
266 return self._invoke(args, False, False)
267 else:
268 return True
271 class Mozilla(UnixBrowser):
272 """Launcher class for Mozilla/Netscape browsers."""
274 raise_opts = ["-noraise", "-raise"]
276 remote_args = ['-remote', 'openURL(%s%action)']
277 remote_action = ""
278 remote_action_newwin = ",new-window"
279 remote_action_newtab = ",new-tab"
281 background = True
283 Netscape = Mozilla
286 class Galeon(UnixBrowser):
287 """Launcher class for Galeon/Epiphany browsers."""
289 raise_opts = ["-noraise", ""]
290 remote_args = ['%action', '%s']
291 remote_action = "-n"
292 remote_action_newwin = "-w"
294 background = True
297 class Opera(UnixBrowser):
298 "Launcher class for Opera browser."
300 raise_opts = ["", "-raise"]
302 remote_args = ['-remote', 'openURL(%s%action)']
303 remote_action = ""
304 remote_action_newwin = ",new-window"
305 remote_action_newtab = ",new-page"
306 background = True
309 class Elinks(UnixBrowser):
310 "Launcher class for Elinks browsers."
312 remote_args = ['-remote', 'openURL(%s%action)']
313 remote_action = ""
314 remote_action_newwin = ",new-window"
315 remote_action_newtab = ",new-tab"
316 background = False
318 # elinks doesn't like its stdout to be redirected -
319 # it uses redirected stdout as a signal to do -dump
320 redirect_stdout = False
323 class Konqueror(BaseBrowser):
324 """Controller for the KDE File Manager (kfm, or Konqueror).
326 See the output of ``kfmclient --commands``
327 for more information on the Konqueror remote-control interface.
330 def open(self, url, new=0, autoraise=1):
331 # XXX Currently I know no way to prevent KFM from opening a new win.
332 if new == 2:
333 action = "newTab"
334 else:
335 action = "openURL"
337 devnull = file(os.devnull, "r+")
338 # if possible, put browser in separate process group, so
339 # keyboard interrupts don't affect browser as well as Python
340 setsid = getattr(os, 'setsid', None)
341 if not setsid:
342 setsid = getattr(os, 'setpgrp', None)
344 try:
345 p = subprocess.Popen(["kfmclient", action, url],
346 close_fds=True, stdin=devnull,
347 stdout=devnull, stderr=devnull)
348 except OSError:
349 # fall through to next variant
350 pass
351 else:
352 p.wait()
353 # kfmclient's return code unfortunately has no meaning as it seems
354 return True
356 try:
357 p = subprocess.Popen(["konqueror", "--silent", url],
358 close_fds=True, stdin=devnull,
359 stdout=devnull, stderr=devnull,
360 preexec_fn=setsid)
361 except OSError:
362 # fall through to next variant
363 pass
364 else:
365 if p.poll() is None:
366 # Should be running now.
367 return True
369 try:
370 p = subprocess.Popen(["kfm", "-d", url],
371 close_fds=True, stdin=devnull,
372 stdout=devnull, stderr=devnull,
373 preexec_fn=setsid)
374 except OSError:
375 return False
376 else:
377 return (p.poll() is None)
380 class Grail(BaseBrowser):
381 # There should be a way to maintain a connection to Grail, but the
382 # Grail remote control protocol doesn't really allow that at this
383 # point. It probably never will!
384 def _find_grail_rc(self):
385 import glob
386 import pwd
387 import socket
388 import tempfile
389 tempdir = os.path.join(tempfile.gettempdir(),
390 ".grail-unix")
391 user = pwd.getpwuid(os.getuid())[0]
392 filename = os.path.join(tempdir, user + "-*")
393 maybes = glob.glob(filename)
394 if not maybes:
395 return None
396 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
397 for fn in maybes:
398 # need to PING each one until we find one that's live
399 try:
400 s.connect(fn)
401 except socket.error:
402 # no good; attempt to clean it out, but don't fail:
403 try:
404 os.unlink(fn)
405 except IOError:
406 pass
407 else:
408 return s
410 def _remote(self, action):
411 s = self._find_grail_rc()
412 if not s:
413 return 0
414 s.send(action)
415 s.close()
416 return 1
418 def open(self, url, new=0, autoraise=1):
419 if new:
420 ok = self._remote("LOADNEW " + url)
421 else:
422 ok = self._remote("LOAD " + url)
423 return ok
427 # Platform support for Unix
430 # These are the right tests because all these Unix browsers require either
431 # a console terminal or an X display to run.
433 def register_X_browsers():
434 # The default Gnome browser
435 if _iscommand("gconftool-2"):
436 # get the web browser string from gconftool
437 gc = 'gconftool-2 -g /desktop/gnome/url-handlers/http/command 2>/dev/null'
438 out = os.popen(gc)
439 commd = out.read().strip()
440 retncode = out.close()
442 # if successful, register it
443 if retncode is None and commd:
444 register("gnome", None, BackgroundBrowser(commd))
446 # First, the Mozilla/Netscape browsers
447 for browser in ("mozilla-firefox", "firefox",
448 "mozilla-firebird", "firebird",
449 "seamonkey", "mozilla", "netscape"):
450 if _iscommand(browser):
451 register(browser, None, Mozilla(browser))
453 # Konqueror/kfm, the KDE browser.
454 if _iscommand("kfm"):
455 register("kfm", Konqueror, Konqueror("kfm"))
456 elif _iscommand("konqueror"):
457 register("konqueror", Konqueror, Konqueror("konqueror"))
459 # Gnome's Galeon and Epiphany
460 for browser in ("galeon", "epiphany"):
461 if _iscommand(browser):
462 register(browser, None, Galeon(browser))
464 # Skipstone, another Gtk/Mozilla based browser
465 if _iscommand("skipstone"):
466 register("skipstone", None, BackgroundBrowser("skipstone"))
468 # Opera, quite popular
469 if _iscommand("opera"):
470 register("opera", None, Opera("opera"))
472 # Next, Mosaic -- old but still in use.
473 if _iscommand("mosaic"):
474 register("mosaic", None, BackgroundBrowser("mosaic"))
476 # Grail, the Python browser. Does anybody still use it?
477 if _iscommand("grail"):
478 register("grail", Grail, None)
480 # Prefer X browsers if present
481 if os.environ.get("DISPLAY"):
482 register_X_browsers()
484 # Also try console browsers
485 if os.environ.get("TERM"):
486 # The Links/elinks browsers <http://artax.karlin.mff.cuni.cz/~mikulas/links/>
487 if _iscommand("links"):
488 register("links", None, GenericBrowser("links"))
489 if _iscommand("elinks"):
490 register("elinks", None, Elinks("elinks"))
491 # The Lynx browser <http://lynx.isc.org/>, <http://lynx.browser.org/>
492 if _iscommand("lynx"):
493 register("lynx", None, GenericBrowser("lynx"))
494 # The w3m browser <http://w3m.sourceforge.net/>
495 if _iscommand("w3m"):
496 register("w3m", None, GenericBrowser("w3m"))
499 # Platform support for Windows
502 if sys.platform[:3] == "win":
503 class WindowsDefault(BaseBrowser):
504 def open(self, url, new=0, autoraise=1):
505 os.startfile(url)
506 return True # Oh, my...
508 _tryorder = []
509 _browsers = {}
510 # Prefer mozilla/netscape/opera if present
511 for browser in ("firefox", "firebird", "seamonkey", "mozilla",
512 "netscape", "opera"):
513 if _iscommand(browser):
514 register(browser, None, BackgroundBrowser(browser))
515 register("windows-default", WindowsDefault)
518 # Platform support for MacOS
521 try:
522 import ic
523 except ImportError:
524 pass
525 else:
526 class InternetConfig(BaseBrowser):
527 def open(self, url, new=0, autoraise=1):
528 ic.launchurl(url)
529 return True # Any way to get status?
531 register("internet-config", InternetConfig, update_tryorder=-1)
533 if sys.platform == 'darwin':
534 # Adapted from patch submitted to SourceForge by Steven J. Burr
535 class MacOSX(BaseBrowser):
536 """Launcher class for Aqua browsers on Mac OS X
538 Optionally specify a browser name on instantiation. Note that this
539 will not work for Aqua browsers if the user has moved the application
540 package after installation.
542 If no browser is specified, the default browser, as specified in the
543 Internet System Preferences panel, will be used.
545 def __init__(self, name):
546 self.name = name
548 def open(self, url, new=0, autoraise=1):
549 assert "'" not in url
550 # hack for local urls
551 if not ':' in url:
552 url = 'file:'+url
554 # new must be 0 or 1
555 new = int(bool(new))
556 if self.name == "default":
557 # User called open, open_new or get without a browser parameter
558 script = 'open location "%s"' % url.replace('"', '%22') # opens in default browser
559 else:
560 # User called get and chose a browser
561 if self.name == "OmniWeb":
562 toWindow = ""
563 else:
564 # Include toWindow parameter of OpenURL command for browsers
565 # that support it. 0 == new window; -1 == existing
566 toWindow = "toWindow %d" % (new - 1)
567 cmd = 'OpenURL "%s"' % url.replace('"', '%22')
568 script = '''tell application "%s"
569 activate
570 %s %s
571 end tell''' % (self.name, cmd, toWindow)
572 # Open pipe to AppleScript through osascript command
573 osapipe = os.popen("osascript", "w")
574 if osapipe is None:
575 return False
576 # Write script to osascript's stdin
577 osapipe.write(script)
578 rc = osapipe.close()
579 return not rc
581 # Don't clear _tryorder or _browsers since OS X can use above Unix support
582 # (but we prefer using the OS X specific stuff)
583 register("MacOSX", None, MacOSX('default'), -1)
587 # Platform support for OS/2
590 if sys.platform[:3] == "os2" and _iscommand("netscape"):
591 _tryorder = []
592 _browsers = {}
593 register("os2netscape", None,
594 GenericBrowser(["start", "netscape", "%s"]), -1)
597 # OK, now that we know what the default preference orders for each
598 # platform are, allow user to override them with the BROWSER variable.
599 if "BROWSER" in os.environ:
600 _userchoices = os.environ["BROWSER"].split(os.pathsep)
601 _userchoices.reverse()
603 # Treat choices in same way as if passed into get() but do register
604 # and prepend to _tryorder
605 for cmdline in _userchoices:
606 if cmdline != '':
607 _synthesize(cmdline, -1)
608 cmdline = None # to make del work if _userchoices was empty
609 del cmdline
610 del _userchoices
612 # what to do if _tryorder is now empty?
615 def main():
616 import getopt
617 usage = """Usage: %s [-n | -t] url
618 -n: open new window
619 -t: open new tab""" % sys.argv[0]
620 try:
621 opts, args = getopt.getopt(sys.argv[1:], 'ntd')
622 except getopt.error, msg:
623 print >>sys.stderr, msg
624 print >>sys.stderr, usage
625 sys.exit(1)
626 new_win = 0
627 for o, a in opts:
628 if o == '-n': new_win = 1
629 elif o == '-t': new_win = 2
630 if len(args) <> 1:
631 print >>sys.stderr, usage
632 sys.exit(1)
634 url = args[0]
635 open(url, new_win)
637 print "\a"
639 if __name__ == "__main__":
640 main()