Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
webbrowser.py763 linesDownload Raw Back to Lib
1"""Interfaces for launching and remotely controlling web browsers."""2# Maintained by Georg Brandl.3 4import os5import shlex6import shutil7import sys8import subprocess9import threading10 11__all__ = ["Error", "open", "open_new", "open_new_tab", "get", "register"]12 13 14class Error(Exception):15    pass16 17 18_lock = threading.RLock()19_browsers = {}                  # Dictionary of available browser controllers20_tryorder = None                # Preference order of available browsers21_os_preferred_browser = None    # The preferred browser22 23 24def register(name, klass, instance=None, *, preferred=False):25    """Register a browser connector."""26    with _lock:27        if _tryorder is None:28            register_standard_browsers()29        _browsers[name.lower()] = [klass, instance]30 31        # Preferred browsers go to the front of the list.32        # Need to match to the default browser returned by xdg-settings, which33        # may be of the form e.g. "firefox.desktop".34        if preferred or (_os_preferred_browser and f'{name}.desktop' == _os_preferred_browser):35            _tryorder.insert(0, name)36        else:37            _tryorder.append(name)38 39 40def get(using=None):41    """Return a browser launcher instance appropriate for the environment."""42    if _tryorder is None:43        with _lock:44            if _tryorder is None:45                register_standard_browsers()46    if using is not None:47        alternatives = [using]48    else:49        alternatives = _tryorder50    for browser in alternatives:51        if '%s' in browser:52            # User gave us a command line, split it into name and args53            browser = shlex.split(browser)54            if browser[-1] == '&':55                return BackgroundBrowser(browser[:-1])56            else:57                return GenericBrowser(browser)58        else:59            # User gave us a browser name or path.60            try:61                command = _browsers[browser.lower()]62            except KeyError:63                command = _synthesize(browser)64            if command[1] is not None:65                return command[1]66            elif command[0] is not None:67                return command[0]()68    raise Error("could not locate runnable browser")69 70 71# Please note: the following definition hides a builtin function.72# It is recommended one does "import webbrowser" and uses webbrowser.open(url)73# instead of "from webbrowser import *".74 75def open(url, new=0, autoraise=True):76    """Display url using the default browser.77 78    If possible, open url in a location determined by new.79    - 0: the same browser window (the default).80    - 1: a new browser window.81    - 2: a new browser page ("tab").82    If possible, autoraise raises the window (the default) or not.83 84    If opening the browser succeeds, return True.85    If there is a problem, return False.86    """87    if _tryorder is None:88        with _lock:89            if _tryorder is None:90                register_standard_browsers()91    for name in _tryorder:92        browser = get(name)93        if browser.open(url, new, autoraise):94            return True95    return False96 97 98def open_new(url):99    """Open url in a new window of the default browser.100 101    If not possible, then open url in the only browser window.102    """103    return open(url, 1)104 105 106def open_new_tab(url):107    """Open url in a new page ("tab") of the default browser.108 109    If not possible, then the behavior becomes equivalent to open_new().110    """111    return open(url, 2)112 113 114def _synthesize(browser, *, preferred=False):115    """Attempt to synthesize a controller based on existing controllers.116 117    This is useful to create a controller when a user specifies a path to118    an entry in the BROWSER environment variable -- we can copy a general119    controller to operate using a specific installation of the desired120    browser in this way.121 122    If we can't create a controller in this way, or if there is no123    executable for the requested browser, return [None, None].124 125    """126    cmd = browser.split()[0]127    if not shutil.which(cmd):128        return [None, None]129    name = os.path.basename(cmd)130    try:131        command = _browsers[name.lower()]132    except KeyError:133        return [None, None]134    # now attempt to clone to fit the new name:135    controller = command[1]136    if controller and name.lower() == controller.basename:137        import copy138        controller = copy.copy(controller)139        controller.name = browser140        controller.basename = os.path.basename(browser)141        register(browser, None, instance=controller, preferred=preferred)142        return [None, controller]143    return [None, None]144 145 146# General parent classes147 148class BaseBrowser:149    """Parent class for all browsers. Do not use directly."""150 151    args = ['%s']152 153    def __init__(self, name=""):154        self.name = name155        self.basename = name156 157    def open(self, url, new=0, autoraise=True):158        raise NotImplementedError159 160    def open_new(self, url):161        return self.open(url, 1)162 163    def open_new_tab(self, url):164        return self.open(url, 2)165 166    @staticmethod167    def _check_url(url):168        """Ensures that the URL is safe to pass to subprocesses as a parameter"""169        if url and url.lstrip().startswith("-"):170            raise ValueError(f"Invalid URL (leading dash disallowed): {url!r}")171 172 173class GenericBrowser(BaseBrowser):174    """Class for all browsers started with a command175       and without remote functionality."""176 177    def __init__(self, name):178        if isinstance(name, str):179            self.name = name180            self.args = ["%s"]181        else:182            # name should be a list with arguments183            self.name = name[0]184            self.args = name[1:]185        self.basename = os.path.basename(self.name)186 187    def open(self, url, new=0, autoraise=True):188        sys.audit("webbrowser.open", url)189        self._check_url(url)190        cmdline = [self.name] + [arg.replace("%s", url)191                                 for arg in self.args]192        try:193            if sys.platform[:3] == 'win':194                p = subprocess.Popen(cmdline)195            else:196                p = subprocess.Popen(cmdline, close_fds=True)197            return not p.wait()198        except OSError:199            return False200 201 202class BackgroundBrowser(GenericBrowser):203    """Class for all browsers which are to be started in the204       background."""205 206    def open(self, url, new=0, autoraise=True):207        cmdline = [self.name] + [arg.replace("%s", url)208                                 for arg in self.args]209        sys.audit("webbrowser.open", url)210        self._check_url(url)211        try:212            if sys.platform[:3] == 'win':213                p = subprocess.Popen(cmdline)214            else:215                p = subprocess.Popen(cmdline, close_fds=True,216                                     start_new_session=True)217            return p.poll() is None218        except OSError:219            return False220 221 222class UnixBrowser(BaseBrowser):223    """Parent class for all Unix browsers with remote functionality."""224 225    raise_opts = None226    background = False227    redirect_stdout = True228    # In remote_args, %s will be replaced with the requested URL.  %action will229    # be replaced depending on the value of 'new' passed to open.230    # remote_action is used for new=0 (open).  If newwin is not None, it is231    # used for new=1 (open_new).  If newtab is not None, it is used for232    # new=3 (open_new_tab).  After both substitutions are made, any empty233    # strings in the transformed remote_args list will be removed.234    remote_args = ['%action', '%s']235    remote_action = None236    remote_action_newwin = None237    remote_action_newtab = None238 239    def _invoke(self, args, remote, autoraise, url=None):240        raise_opt = []241        if remote and self.raise_opts:242            # use autoraise argument only for remote invocation243            autoraise = int(autoraise)244            opt = self.raise_opts[autoraise]245            if opt:246                raise_opt = [opt]247 248        cmdline = [self.name] + raise_opt + args249 250        if remote or self.background:251            inout = subprocess.DEVNULL252        else:253            # for TTY browsers, we need stdin/out254            inout = None255        p = subprocess.Popen(cmdline, close_fds=True, stdin=inout,256                             stdout=(self.redirect_stdout and inout or None),257                             stderr=inout, start_new_session=True)258        if remote:259            # wait at most five seconds. If the subprocess is not finished, the260            # remote invocation has (hopefully) started a new instance.261            try:262                rc = p.wait(5)263                # if remote call failed, open() will try direct invocation264                return not rc265            except subprocess.TimeoutExpired:266                return True267        elif self.background:268            if p.poll() is None:269                return True270            else:271                return False272        else:273            return not p.wait()274 275    def open(self, url, new=0, autoraise=True):276        sys.audit("webbrowser.open", url)277        self._check_url(url)278        if new == 0:279            action = self.remote_action280        elif new == 1:281            action = self.remote_action_newwin282        elif new == 2:283            if self.remote_action_newtab is None:284                action = self.remote_action_newwin285            else:286                action = self.remote_action_newtab287        else:288            raise Error("Bad 'new' parameter to open(); "289                        f"expected 0, 1, or 2, got {new}")290 291        args = [arg.replace("%s", url).replace("%action", action)292                for arg in self.remote_args]293        args = [arg for arg in args if arg]294        success = self._invoke(args, True, autoraise, url)295        if not success:296            # remote invocation failed, try straight way297            args = [arg.replace("%s", url) for arg in self.args]298            return self._invoke(args, False, False)299        else:300            return True301 302 303class Mozilla(UnixBrowser):304    """Launcher class for Mozilla browsers."""305 306    remote_args = ['%action', '%s']307    remote_action = ""308    remote_action_newwin = "-new-window"309    remote_action_newtab = "-new-tab"310    background = True311 312 313class Epiphany(UnixBrowser):314    """Launcher class for Epiphany browser."""315 316    raise_opts = ["-noraise", ""]317    remote_args = ['%action', '%s']318    remote_action = "-n"319    remote_action_newwin = "-w"320    background = True321 322 323class Chrome(UnixBrowser):324    """Launcher class for Google Chrome browser."""325 326    remote_args = ['%action', '%s']327    remote_action = ""328    remote_action_newwin = "--new-window"329    remote_action_newtab = ""330    background = True331 332 333Chromium = Chrome334 335 336class Opera(UnixBrowser):337    """Launcher class for Opera browser."""338 339    remote_args = ['%action', '%s']340    remote_action = ""341    remote_action_newwin = "--new-window"342    remote_action_newtab = ""343    background = True344 345 346class Elinks(UnixBrowser):347    """Launcher class for Elinks browsers."""348 349    remote_args = ['-remote', 'openURL(%s%action)']350    remote_action = ""351    remote_action_newwin = ",new-window"352    remote_action_newtab = ",new-tab"353    background = False354 355    # elinks doesn't like its stdout to be redirected -356    # it uses redirected stdout as a signal to do -dump357    redirect_stdout = False358 359 360class Konqueror(BaseBrowser):361    """Controller for the KDE File Manager (kfm, or Konqueror).362 363    See the output of ``kfmclient --commands``364    for more information on the Konqueror remote-control interface.365    """366 367    def open(self, url, new=0, autoraise=True):368        sys.audit("webbrowser.open", url)369        self._check_url(url)370        # XXX Currently I know no way to prevent KFM from opening a new win.371        if new == 2:372            action = "newTab"373        else:374            action = "openURL"375 376        devnull = subprocess.DEVNULL377 378        try:379            p = subprocess.Popen(["kfmclient", action, url],380                                 close_fds=True, stdin=devnull,381                                 stdout=devnull, stderr=devnull)382        except OSError:383            # fall through to next variant384            pass385        else:386            p.wait()387            # kfmclient's return code unfortunately has no meaning as it seems388            return True389 390        try:391            p = subprocess.Popen(["konqueror", "--silent", url],392                                 close_fds=True, stdin=devnull,393                                 stdout=devnull, stderr=devnull,394                                 start_new_session=True)395        except OSError:396            # fall through to next variant397            pass398        else:399            if p.poll() is None:400                # Should be running now.401                return True402 403        try:404            p = subprocess.Popen(["kfm", "-d", url],405                                 close_fds=True, stdin=devnull,406                                 stdout=devnull, stderr=devnull,407                                 start_new_session=True)408        except OSError:409            return False410        else:411            return p.poll() is None412 413 414class Edge(UnixBrowser):415    """Launcher class for Microsoft Edge browser."""416 417    remote_args = ['%action', '%s']418    remote_action = ""419    remote_action_newwin = "--new-window"420    remote_action_newtab = ""421    background = True422 423 424#425# Platform support for Unix426#427 428# These are the right tests because all these Unix browsers require either429# a console terminal or an X display to run.430 431def register_X_browsers():432 433    # use xdg-open if around434    if shutil.which("xdg-open"):435        register("xdg-open", None, BackgroundBrowser("xdg-open"))436 437    # Opens an appropriate browser for the URL scheme according to438    # freedesktop.org settings (GNOME, KDE, XFCE, etc.)439    if shutil.which("gio"):440        register("gio", None, BackgroundBrowser(["gio", "open", "--", "%s"]))441 442    xdg_desktop = os.getenv("XDG_CURRENT_DESKTOP", "").split(":")443 444    # The default GNOME3 browser445    if (("GNOME" in xdg_desktop or446         "GNOME_DESKTOP_SESSION_ID" in os.environ) and447            shutil.which("gvfs-open")):448        register("gvfs-open", None, BackgroundBrowser("gvfs-open"))449 450    # The default KDE browser451    if (("KDE" in xdg_desktop or452         "KDE_FULL_SESSION" in os.environ) and453            shutil.which("kfmclient")):454        register("kfmclient", Konqueror, Konqueror("kfmclient"))455 456    # Common symbolic link for the default X11 browser457    if shutil.which("x-www-browser"):458        register("x-www-browser", None, BackgroundBrowser("x-www-browser"))459 460    # The Mozilla browsers461    for browser in ("firefox", "iceweasel", "seamonkey", "mozilla-firefox",462                    "mozilla"):463        if shutil.which(browser):464            register(browser, None, Mozilla(browser))465 466    # Konqueror/kfm, the KDE browser.467    if shutil.which("kfm"):468        register("kfm", Konqueror, Konqueror("kfm"))469    elif shutil.which("konqueror"):470        register("konqueror", Konqueror, Konqueror("konqueror"))471 472    # Gnome's Epiphany473    if shutil.which("epiphany"):474        register("epiphany", None, Epiphany("epiphany"))475 476    # Google Chrome/Chromium browsers477    for browser in ("google-chrome", "chrome", "chromium", "chromium-browser"):478        if shutil.which(browser):479            register(browser, None, Chrome(browser))480 481    # Opera, quite popular482    if shutil.which("opera"):483        register("opera", None, Opera("opera"))484 485    if shutil.which("microsoft-edge"):486        register("microsoft-edge", None, Edge("microsoft-edge"))487 488 489def register_standard_browsers():490    global _tryorder491    _tryorder = []492 493    if sys.platform == 'darwin':494        register("MacOSX", None, MacOSXOSAScript('default'))495        register("chrome", None, MacOSXOSAScript('google chrome'))496        register("firefox", None, MacOSXOSAScript('firefox'))497        register("safari", None, MacOSXOSAScript('safari'))498        # macOS can use below Unix support (but we prefer using the macOS499        # specific stuff)500 501    if sys.platform == "ios":502        register("iosbrowser", None, IOSBrowser(), preferred=True)503 504    if sys.platform == "serenityos":505        # SerenityOS webbrowser, simply called "Browser".506        register("Browser", None, BackgroundBrowser("Browser"))507 508    if sys.platform[:3] == "win":509        # First try to use the default Windows browser510        register("windows-default", WindowsDefault)511 512        # Detect some common Windows browsers, fallback to Microsoft Edge513        # location in 64-bit Windows514        edge64 = os.path.join(os.environ.get("PROGRAMFILES(x86)", "C:\\Program Files (x86)"),515                              "Microsoft\\Edge\\Application\\msedge.exe")516        # location in 32-bit Windows517        edge32 = os.path.join(os.environ.get("PROGRAMFILES", "C:\\Program Files"),518                              "Microsoft\\Edge\\Application\\msedge.exe")519        for browser in ("firefox", "seamonkey", "mozilla", "chrome",520                        "opera", edge64, edge32):521            if shutil.which(browser):522                register(browser, None, BackgroundBrowser(browser))523        if shutil.which("MicrosoftEdge.exe"):524            register("microsoft-edge", None, Edge("MicrosoftEdge.exe"))525    else:526        # Prefer X browsers if present527        #528        # NOTE: Do not check for X11 browser on macOS,529        # XQuartz installation sets a DISPLAY environment variable and will530        # autostart when someone tries to access the display. Mac users in531        # general don't need an X11 browser.532        if sys.platform != "darwin" and (os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY")):533            try:534                cmd = "xdg-settings get default-web-browser".split()535                raw_result = subprocess.check_output(cmd, stderr=subprocess.DEVNULL)536                result = raw_result.decode().strip()537            except (FileNotFoundError, subprocess.CalledProcessError,538                    PermissionError, NotADirectoryError):539                pass540            else:541                global _os_preferred_browser542                _os_preferred_browser = result543 544            register_X_browsers()545 546        # Also try console browsers547        if os.environ.get("TERM"):548            # Common symbolic link for the default text-based browser549            if shutil.which("www-browser"):550                register("www-browser", None, GenericBrowser("www-browser"))551            # The Links/elinks browsers <http://links.twibright.com/>552            if shutil.which("links"):553                register("links", None, GenericBrowser("links"))554            if shutil.which("elinks"):555                register("elinks", None, Elinks("elinks"))556            # The Lynx browser <https://lynx.invisible-island.net/>, <http://lynx.browser.org/>557            if shutil.which("lynx"):558                register("lynx", None, GenericBrowser("lynx"))559            # The w3m browser <http://w3m.sourceforge.net/>560            if shutil.which("w3m"):561                register("w3m", None, GenericBrowser("w3m"))562 563    # OK, now that we know what the default preference orders for each564    # platform are, allow user to override them with the BROWSER variable.565    if "BROWSER" in os.environ:566        userchoices = os.environ["BROWSER"].split(os.pathsep)567        userchoices.reverse()568 569        # Treat choices in same way as if passed into get() but do register570        # and prepend to _tryorder571        for cmdline in userchoices:572            if all(x not in cmdline for x in " \t"):573                # Assume this is the name of a registered command, use574                # that unless it is a GenericBrowser.575                try:576                    command = _browsers[cmdline.lower()]577                except KeyError:578                    pass579 580                else:581                    if not isinstance(command[1], GenericBrowser):582                        _tryorder.insert(0, cmdline.lower())583                        continue584 585            if cmdline != '':586                cmd = _synthesize(cmdline, preferred=True)587                if cmd[1] is None:588                    register(cmdline, None, GenericBrowser(cmdline), preferred=True)589 590    # what to do if _tryorder is now empty?591 592 593#594# Platform support for Windows595#596 597if sys.platform[:3] == "win":598    class WindowsDefault(BaseBrowser):599        def open(self, url, new=0, autoraise=True):600            sys.audit("webbrowser.open", url)601            self._check_url(url)602            try:603                os.startfile(url)604            except OSError:605                # [Error 22] No application is associated with the specified606                # file for this operation: '<URL>'607                return False608            else:609                return True610 611#612# Platform support for macOS613#614 615if sys.platform == 'darwin':616    class MacOSXOSAScript(BaseBrowser):617        def __init__(self, name='default'):618            super().__init__(name)619 620        def open(self, url, new=0, autoraise=True):621            sys.audit("webbrowser.open", url)622            self._check_url(url)623            url = url.replace('"', '%22')624            if self.name == 'default':625                proto, _sep, _rest = url.partition(":")626                if _sep and proto.lower() in {"http", "https"}:627                    # default web URL, don't need to lookup browser628                    script = f'open location "{url}"'629                else:630                    # if not a web URL, need to lookup default browser to ensure a browser is launched631                    # this should always work, but is overkill to lookup http handler632                    # before launching http633                    script = f"""634                        use framework "AppKit"635                        use AppleScript version "2.4"636                        use scripting additions637 638                        property NSWorkspace : a reference to current application's NSWorkspace639                        property NSURL : a reference to current application's NSURL640 641                        set http_url to NSURL's URLWithString:"https://python.org"642                        set browser_url to (NSWorkspace's sharedWorkspace)'s ¬643                            URLForApplicationToOpenURL:http_url644                        set app_path to browser_url's relativePath as text -- NSURL to absolute path '/Applications/Safari.app'645 646                        tell application app_path647                            activate648                            open location "{url}"649                        end tell650                    """651            else:652                script = f'''653                   tell application "{self.name}"654                       activate655                       open location "{url}"656                   end657                   '''658 659            osapipe = os.popen("/usr/bin/osascript", "w")660            if osapipe is None:661                return False662 663            osapipe.write(script)664            rc = osapipe.close()665            return not rc666 667#668# Platform support for iOS669#670if sys.platform == "ios":671    from _ios_support import objc672    if objc:673        # If objc exists, we know ctypes is also importable.674        from ctypes import c_void_p, c_char_p, c_ulong675 676    class IOSBrowser(BaseBrowser):677        def open(self, url, new=0, autoraise=True):678            sys.audit("webbrowser.open", url)679            self._check_url(url)680            # If ctypes isn't available, we can't open a browser681            if objc is None:682                return False683 684            # All the messages in this call return object references.685            objc.objc_msgSend.restype = c_void_p686 687            # This is the equivalent of:688            #    NSString url_string =689            #        [NSString stringWithCString:url.encode("utf-8")690            #                           encoding:NSUTF8StringEncoding];691            NSString = objc.objc_getClass(b"NSString")692            constructor = objc.sel_registerName(b"stringWithCString:encoding:")693            objc.objc_msgSend.argtypes = [c_void_p, c_void_p, c_char_p, c_ulong]694            url_string = objc.objc_msgSend(695                NSString,696                constructor,697                url.encode("utf-8"),698                4,  # NSUTF8StringEncoding = 4699            )700 701            # Create an NSURL object representing the URL702            # This is the equivalent of:703            #   NSURL *nsurl = [NSURL URLWithString:url];704            NSURL = objc.objc_getClass(b"NSURL")705            urlWithString_ = objc.sel_registerName(b"URLWithString:")706            objc.objc_msgSend.argtypes = [c_void_p, c_void_p, c_void_p]707            ns_url = objc.objc_msgSend(NSURL, urlWithString_, url_string)708 709            # Get the shared UIApplication instance710            # This code is the equivalent of:711            # UIApplication shared_app = [UIApplication sharedApplication]712            UIApplication = objc.objc_getClass(b"UIApplication")713            sharedApplication = objc.sel_registerName(b"sharedApplication")714            objc.objc_msgSend.argtypes = [c_void_p, c_void_p]715            shared_app = objc.objc_msgSend(UIApplication, sharedApplication)716 717            # Open the URL on the shared application718            # This code is the equivalent of:719            #   [shared_app openURL:ns_url720            #               options:NIL721            #     completionHandler:NIL];722            openURL_ = objc.sel_registerName(b"openURL:options:completionHandler:")723            objc.objc_msgSend.argtypes = [724                c_void_p, c_void_p, c_void_p, c_void_p, c_void_p725            ]726            # Method returns void727            objc.objc_msgSend.restype = None728            objc.objc_msgSend(shared_app, openURL_, ns_url, None, None)729 730            return True731 732 733def parse_args(arg_list: list[str] | None):734    import argparse735    parser = argparse.ArgumentParser(736        description="Open URL in a web browser.", color=True,737    )738    parser.add_argument("url", help="URL to open")739 740    group = parser.add_mutually_exclusive_group()741    group.add_argument("-n", "--new-window", action="store_const",742                       const=1, default=0, dest="new_win",743                       help="open new window")744    group.add_argument("-t", "--new-tab", action="store_const",745                       const=2, default=0, dest="new_win",746                       help="open new tab")747 748    args = parser.parse_args(arg_list)749 750    return args751 752 753def main(arg_list: list[str] | None = None):754    args = parse_args(arg_list)755 756    open(args.url, args.new_win)757 758    print("\a")759 760 761if __name__ == "__main__":762    main()763 
codekingpro/portable-devtools · Team Ai