From 00b28bf802cb5049046d08c3b707f5dc45ad563f Mon Sep 17 00:00:00 2001 From: Patrick Arminio Date: Wed, 30 Oct 2024 12:54:59 +0000 Subject: [PATCH 1/5] =?UTF-8?q?=E2=9C=A8=20Add=20`open=5Fbrowser`=20functi?= =?UTF-8?q?on?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This replaces `webbrowser.open` in order to prevent spammy messages being shown on the terminal. --- tests/test_browser.py | 44 +++++++++++++++++++++++++++++++++++++++++ typer/__init__.py | 1 + typer/browser.py | 46 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+) create mode 100644 tests/test_browser.py create mode 100644 typer/browser.py diff --git a/tests/test_browser.py b/tests/test_browser.py new file mode 100644 index 0000000000..19102d305a --- /dev/null +++ b/tests/test_browser.py @@ -0,0 +1,44 @@ +import subprocess +from unittest.mock import patch + +import pytest +import typer + +url = "http://example.com" + + +@pytest.mark.parametrize( + "system, command", + [ + ("Darwin", "open"), + ("Linux", "xdg-open"), + ("FreeBSD", "xdg-open"), + ], +) +def test_open_browser_unix(system: str, command: str): + with patch("platform.system", return_value=system), patch( + "shutil.which", return_value=True + ), patch("subprocess.Popen") as mock_popen: + typer.open_browser(url) + + mock_popen.assert_called_once_with( + [command, url], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT + ) + + +def test_open_browser_windows(): + with patch("platform.system", return_value="Windows"), patch( + "webbrowser.open" + ) as mock_webbrowser_open: + typer.open_browser(url) + + mock_webbrowser_open.assert_called_once_with(url) + + +def test_open_browser_no_xdg_open(): + with patch("platform.system", return_value="Linux"), patch( + "shutil.which", return_value=None + ), patch("webbrowser.open") as mock_webbrowser_open: + typer.open_browser(url) + + mock_webbrowser_open.assert_called_once_with(url) diff --git a/typer/__init__.py b/typer/__init__.py index b422dd00d3..fa7d379f7b 100644 --- a/typer/__init__.py +++ b/typer/__init__.py @@ -37,3 +37,4 @@ from .models import FileTextWrite as FileTextWrite from .params import Argument as Argument from .params import Option as Option +from .browser import open_browser as open_browser diff --git a/typer/browser.py b/typer/browser.py new file mode 100644 index 0000000000..2061c9cb78 --- /dev/null +++ b/typer/browser.py @@ -0,0 +1,46 @@ +import platform +import shutil +import subprocess + + +def _is_macos() -> bool: + return platform.system() == "Darwin" + + +def _is_linux_or_bsd() -> bool: + if platform.system() == "Linux": + return True + + return "BSD" in platform.system() + + +def open_browser(url: str) -> None: + """ + Open a web browser to the specified URL. + + This function handles different operating systems separately: + - On macOS (Darwin), it uses the 'open' command. + - On Linux and BSD, it uses 'xdg-open' if available. + - On Windows (and other OSes), it uses the standard webbrowser module. + + The function avoids, when possible, using the webbrowser module on Linux and macOS + to prevent spammy terminal messages from some browsers (e.g., Chrome). + """ + + if _is_macos(): + subprocess.Popen( + ["open", url], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT + ) + return + + has_xdg_open = _is_linux_or_bsd() and shutil.which("xdg-open") is not None + + if has_xdg_open: + subprocess.Popen( + ["xdg-open", url], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT + ) + return + + import webbrowser + + webbrowser.open(url) From 60003197f5ef2430ad30000e38af34d72aef9dd8 Mon Sep 17 00:00:00 2001 From: Patrick Arminio Date: Thu, 31 Oct 2024 18:33:46 +0000 Subject: [PATCH 2/5] Replace launch --- tests/{test_browser.py => test_launch.py} | 12 ++-- typer/__init__.py | 3 +- typer/browser.py | 46 --------------- typer/launch.py | 70 +++++++++++++++++++++++ 4 files changed, 77 insertions(+), 54 deletions(-) rename tests/{test_browser.py => test_launch.py} (80%) delete mode 100644 typer/browser.py create mode 100644 typer/launch.py diff --git a/tests/test_browser.py b/tests/test_launch.py similarity index 80% rename from tests/test_browser.py rename to tests/test_launch.py index 19102d305a..4a157011bb 100644 --- a/tests/test_browser.py +++ b/tests/test_launch.py @@ -15,30 +15,30 @@ ("FreeBSD", "xdg-open"), ], ) -def test_open_browser_unix(system: str, command: str): +def test_launch_url_unix(system: str, command: str): with patch("platform.system", return_value=system), patch( "shutil.which", return_value=True ), patch("subprocess.Popen") as mock_popen: - typer.open_browser(url) + typer.launch(url) mock_popen.assert_called_once_with( [command, url], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT ) -def test_open_browser_windows(): +def test_launch_url_windows(): with patch("platform.system", return_value="Windows"), patch( "webbrowser.open" ) as mock_webbrowser_open: - typer.open_browser(url) + typer.launch(url) mock_webbrowser_open.assert_called_once_with(url) -def test_open_browser_no_xdg_open(): +def test_launch_url_no_xdg_open(): with patch("platform.system", return_value="Linux"), patch( "shutil.which", return_value=None ), patch("webbrowser.open") as mock_webbrowser_open: - typer.open_browser(url) + typer.launch(url) mock_webbrowser_open.assert_called_once_with(url) diff --git a/typer/__init__.py b/typer/__init__.py index fa7d379f7b..00e3f0fab6 100644 --- a/typer/__init__.py +++ b/typer/__init__.py @@ -12,7 +12,6 @@ from click.termui import echo_via_pager as echo_via_pager from click.termui import edit as edit from click.termui import getchar as getchar -from click.termui import launch as launch from click.termui import pause as pause from click.termui import progressbar as progressbar from click.termui import prompt as prompt @@ -27,6 +26,7 @@ from click.utils import open_file as open_file from . import colors as colors +from .launch import launch as launch from .main import Typer as Typer from .main import run as run from .models import CallbackParam as CallbackParam @@ -37,4 +37,3 @@ from .models import FileTextWrite as FileTextWrite from .params import Argument as Argument from .params import Option as Option -from .browser import open_browser as open_browser diff --git a/typer/browser.py b/typer/browser.py deleted file mode 100644 index 2061c9cb78..0000000000 --- a/typer/browser.py +++ /dev/null @@ -1,46 +0,0 @@ -import platform -import shutil -import subprocess - - -def _is_macos() -> bool: - return platform.system() == "Darwin" - - -def _is_linux_or_bsd() -> bool: - if platform.system() == "Linux": - return True - - return "BSD" in platform.system() - - -def open_browser(url: str) -> None: - """ - Open a web browser to the specified URL. - - This function handles different operating systems separately: - - On macOS (Darwin), it uses the 'open' command. - - On Linux and BSD, it uses 'xdg-open' if available. - - On Windows (and other OSes), it uses the standard webbrowser module. - - The function avoids, when possible, using the webbrowser module on Linux and macOS - to prevent spammy terminal messages from some browsers (e.g., Chrome). - """ - - if _is_macos(): - subprocess.Popen( - ["open", url], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT - ) - return - - has_xdg_open = _is_linux_or_bsd() and shutil.which("xdg-open") is not None - - if has_xdg_open: - subprocess.Popen( - ["xdg-open", url], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT - ) - return - - import webbrowser - - webbrowser.open(url) diff --git a/typer/launch.py b/typer/launch.py new file mode 100644 index 0000000000..94df38a0f6 --- /dev/null +++ b/typer/launch.py @@ -0,0 +1,70 @@ +import platform +import shutil +import subprocess + +from click import launch as click_launch + + +def _is_macos() -> bool: + return platform.system() == "Darwin" + + +def _is_linux_or_bsd() -> bool: + if platform.system() == "Linux": + return True + + return "BSD" in platform.system() + + +def launch(url: str, wait: bool = False, locate: bool = False) -> int: + """This function launches the given URL (or filename) in the default + viewer application for this file type. If this is an executable, it + might launch the executable in a new session. The return value is + the exit code of the launched application. Usually, ``0`` indicates + success. + + This function handles url in different operating systems separately: + - On macOS (Darwin), it uses the 'open' command. + - On Linux and BSD, it uses 'xdg-open' if available. + - On Windows (and other OSes), it uses the standard webbrowser module. + + The function avoids, when possible, using the webbrowser module on Linux and macOS + to prevent spammy terminal messages from some browsers (e.g., Chrome). + + Examples:: + + typer.launch('https://click.palletsprojects.com/') + typer.launch('/my/downloaded/file', locate=True) + + :param url: URL or filename of the thing to launch. + :param wait: Wait for the program to exit before returning. This + only works if the launched program blocks. In particular, + ``xdg-open`` on Linux does not block. + :param locate: if this is set to `True` then instead of launching the + application associated with the URL it will attempt to + launch a file manager with the file located. This + might have weird effects if the URL does not point to + the filesystem. + """ + + if url.startswith("http://") or url.startswith("https://"): + if _is_macos(): + return subprocess.Popen( + ["open", url], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT + ).wait() + + has_xdg_open = _is_linux_or_bsd() and shutil.which("xdg-open") is not None + + if has_xdg_open: + return subprocess.Popen( + ["xdg-open", url], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT + ).wait() + + import webbrowser + + webbrowser.open(url) + + return 0 + + else: + return click_launch(url) From 05825085929e824517229d94a560f732da1451c2 Mon Sep 17 00:00:00 2001 From: Patrick Arminio Date: Fri, 1 Nov 2024 00:06:41 +0000 Subject: [PATCH 3/5] Add test for non urls --- tests/test_launch.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_launch.py b/tests/test_launch.py index 4a157011bb..ed4d3136a8 100644 --- a/tests/test_launch.py +++ b/tests/test_launch.py @@ -42,3 +42,10 @@ def test_launch_url_no_xdg_open(): typer.launch(url) mock_webbrowser_open.assert_called_once_with(url) + + +def test_calls_original_launch_when_not_passing_urls(): + with patch("typer.launch.click_launch", return_value=0) as launch_mock: + typer.launch("not a url") + + launch_mock.assert_called_once_with("not a url") From 4d30db49715a89c55cef059fae35f2be9e59870b Mon Sep 17 00:00:00 2001 From: Patrick Arminio Date: Fri, 1 Nov 2024 11:01:55 +0000 Subject: [PATCH 4/5] Move to main --- tests/test_launch.py | 2 +- typer/__init__.py | 2 +- typer/launch.py | 70 -------------------------------------------- typer/main.py | 68 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 70 insertions(+), 72 deletions(-) delete mode 100644 typer/launch.py diff --git a/tests/test_launch.py b/tests/test_launch.py index ed4d3136a8..75aaa2f091 100644 --- a/tests/test_launch.py +++ b/tests/test_launch.py @@ -45,7 +45,7 @@ def test_launch_url_no_xdg_open(): def test_calls_original_launch_when_not_passing_urls(): - with patch("typer.launch.click_launch", return_value=0) as launch_mock: + with patch("typer.main.click.launch", return_value=0) as launch_mock: typer.launch("not a url") launch_mock.assert_called_once_with("not a url") diff --git a/typer/__init__.py b/typer/__init__.py index 00e3f0fab6..3d84062a76 100644 --- a/typer/__init__.py +++ b/typer/__init__.py @@ -26,8 +26,8 @@ from click.utils import open_file as open_file from . import colors as colors -from .launch import launch as launch from .main import Typer as Typer +from .main import launch as launch from .main import run as run from .models import CallbackParam as CallbackParam from .models import Context as Context diff --git a/typer/launch.py b/typer/launch.py deleted file mode 100644 index 94df38a0f6..0000000000 --- a/typer/launch.py +++ /dev/null @@ -1,70 +0,0 @@ -import platform -import shutil -import subprocess - -from click import launch as click_launch - - -def _is_macos() -> bool: - return platform.system() == "Darwin" - - -def _is_linux_or_bsd() -> bool: - if platform.system() == "Linux": - return True - - return "BSD" in platform.system() - - -def launch(url: str, wait: bool = False, locate: bool = False) -> int: - """This function launches the given URL (or filename) in the default - viewer application for this file type. If this is an executable, it - might launch the executable in a new session. The return value is - the exit code of the launched application. Usually, ``0`` indicates - success. - - This function handles url in different operating systems separately: - - On macOS (Darwin), it uses the 'open' command. - - On Linux and BSD, it uses 'xdg-open' if available. - - On Windows (and other OSes), it uses the standard webbrowser module. - - The function avoids, when possible, using the webbrowser module on Linux and macOS - to prevent spammy terminal messages from some browsers (e.g., Chrome). - - Examples:: - - typer.launch('https://click.palletsprojects.com/') - typer.launch('/my/downloaded/file', locate=True) - - :param url: URL or filename of the thing to launch. - :param wait: Wait for the program to exit before returning. This - only works if the launched program blocks. In particular, - ``xdg-open`` on Linux does not block. - :param locate: if this is set to `True` then instead of launching the - application associated with the URL it will attempt to - launch a file manager with the file located. This - might have weird effects if the URL does not point to - the filesystem. - """ - - if url.startswith("http://") or url.startswith("https://"): - if _is_macos(): - return subprocess.Popen( - ["open", url], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT - ).wait() - - has_xdg_open = _is_linux_or_bsd() and shutil.which("xdg-open") is not None - - if has_xdg_open: - return subprocess.Popen( - ["xdg-open", url], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT - ).wait() - - import webbrowser - - webbrowser.open(url) - - return 0 - - else: - return click_launch(url) diff --git a/typer/main.py b/typer/main.py index a621bda6ad..03d5e6f620 100644 --- a/typer/main.py +++ b/typer/main.py @@ -1,5 +1,8 @@ import inspect import os +import platform +import shutil +import subprocess import sys import traceback from datetime import datetime @@ -1079,3 +1082,68 @@ def run(function: Callable[..., Any]) -> None: app = Typer(add_completion=False) app.command()(function) app() + + +def _is_macos() -> bool: + return platform.system() == "Darwin" + + +def _is_linux_or_bsd() -> bool: + if platform.system() == "Linux": + return True + + return "BSD" in platform.system() + + +def launch(url: str, wait: bool = False, locate: bool = False) -> int: + """This function launches the given URL (or filename) in the default + viewer application for this file type. If this is an executable, it + might launch the executable in a new session. The return value is + the exit code of the launched application. Usually, ``0`` indicates + success. + + This function handles url in different operating systems separately: + - On macOS (Darwin), it uses the 'open' command. + - On Linux and BSD, it uses 'xdg-open' if available. + - On Windows (and other OSes), it uses the standard webbrowser module. + + The function avoids, when possible, using the webbrowser module on Linux and macOS + to prevent spammy terminal messages from some browsers (e.g., Chrome). + + Examples:: + + typer.launch('https://click.palletsprojects.com/') + typer.launch('/my/downloaded/file', locate=True) + + :param url: URL or filename of the thing to launch. + :param wait: Wait for the program to exit before returning. This + only works if the launched program blocks. In particular, + ``xdg-open`` on Linux does not block. + :param locate: if this is set to `True` then instead of launching the + application associated with the URL it will attempt to + launch a file manager with the file located. This + might have weird effects if the URL does not point to + the filesystem. + """ + + if url.startswith("http://") or url.startswith("https://"): + if _is_macos(): + return subprocess.Popen( + ["open", url], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT + ).wait() + + has_xdg_open = _is_linux_or_bsd() and shutil.which("xdg-open") is not None + + if has_xdg_open: + return subprocess.Popen( + ["xdg-open", url], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT + ).wait() + + import webbrowser + + webbrowser.open(url) + + return 0 + + else: + return click.launch(url) From ffa408095e2f0ec3ff2a4d5e31b5694076002e92 Mon Sep 17 00:00:00 2001 From: Patrick Arminio Date: Fri, 1 Nov 2024 11:03:24 +0000 Subject: [PATCH 5/5] Update docstring --- typer/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/typer/main.py b/typer/main.py index 03d5e6f620..410dfb9257 100644 --- a/typer/main.py +++ b/typer/main.py @@ -1112,8 +1112,8 @@ def launch(url: str, wait: bool = False, locate: bool = False) -> int: Examples:: - typer.launch('https://click.palletsprojects.com/') - typer.launch('/my/downloaded/file', locate=True) + typer.launch("https://typer.tiangolo.com/") + typer.launch("/my/downloaded/file", locate=True) :param url: URL or filename of the thing to launch. :param wait: Wait for the program to exit before returning. This