Skip to content

Commit 00e6af5

Browse files
authored
Merge branch 'master' into feat/autocompletion
2 parents 8b300ba + 299ad70 commit 00e6af5

File tree

8 files changed

+148
-5
lines changed

8 files changed

+148
-5
lines changed

.github/workflows/latest-changes.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ jobs:
3434
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }}
3535
with:
3636
limit-access-to-actor: true
37-
- uses: tiangolo/[email protected].1
37+
- uses: tiangolo/[email protected].2
3838
with:
3939
token: ${{ secrets.GITHUB_TOKEN }}
4040
latest_changes_file: docs/release-notes.md

docs/release-notes.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,17 @@
22

33
## Latest Changes
44

5+
### Internal
6+
7+
* ⬆ Bump tiangolo/latest-changes from 0.3.1 to 0.3.2. PR [#1044](https://github.com/fastapi/typer/pull/1044) by [@dependabot[bot]](https://github.com/apps/dependabot).
8+
* ⬆ Update pytest-cov requirement from <6.0.0,>=2.10.0 to >=2.10.0,<7.0.0. PR [#1033](https://github.com/fastapi/typer/pull/1033) by [@dependabot[bot]](https://github.com/apps/dependabot).
9+
10+
## 0.13.0
11+
512
### Features
613

14+
* ✨ Handle `KeyboardInterrupt` separately from other exceptions. PR [#1039](https://github.com/fastapi/typer/pull/1039) by [@patrick91](https://github.com/patrick91).
15+
* ✨ Update `launch` to not print anything when opening urls. PR [#1035](https://github.com/fastapi/typer/pull/1035) by [@patrick91](https://github.com/patrick91).
716
* ✨ Show help items in order of definition. PR [#944](https://github.com/fastapi/typer/pull/944) by [@svlandeg](https://github.com/svlandeg).
817

918
### Fixes

requirements-tests.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
-e .
22

33
pytest >=4.4.0,<9.0.0
4-
pytest-cov >=2.10.0,<6.0.0
4+
pytest-cov >=2.10.0,<7.0.0
55
coverage[toml] >=6.2,<8.0
66
pytest-xdist >=1.32.0,<4.0.0
77
pytest-sugar >=0.9.4,<1.1.0

tests/test_exit_errors.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,19 @@ def main():
1919
assert result.exit_code == 1
2020

2121

22+
def test_keyboardinterrupt():
23+
# Mainly for coverage/completeness
24+
app = typer.Typer()
25+
26+
@app.command()
27+
def main():
28+
raise KeyboardInterrupt()
29+
30+
result = runner.invoke(app)
31+
assert result.exit_code == 130
32+
assert result.stdout == ""
33+
34+
2235
def test_oserror():
2336
# Mainly for coverage/completeness
2437
app = typer.Typer()

tests/test_launch.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import subprocess
2+
from unittest.mock import patch
3+
4+
import pytest
5+
import typer
6+
7+
url = "http://example.com"
8+
9+
10+
@pytest.mark.parametrize(
11+
"system, command",
12+
[
13+
("Darwin", "open"),
14+
("Linux", "xdg-open"),
15+
("FreeBSD", "xdg-open"),
16+
],
17+
)
18+
def test_launch_url_unix(system: str, command: str):
19+
with patch("platform.system", return_value=system), patch(
20+
"shutil.which", return_value=True
21+
), patch("subprocess.Popen") as mock_popen:
22+
typer.launch(url)
23+
24+
mock_popen.assert_called_once_with(
25+
[command, url], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT
26+
)
27+
28+
29+
def test_launch_url_windows():
30+
with patch("platform.system", return_value="Windows"), patch(
31+
"webbrowser.open"
32+
) as mock_webbrowser_open:
33+
typer.launch(url)
34+
35+
mock_webbrowser_open.assert_called_once_with(url)
36+
37+
38+
def test_launch_url_no_xdg_open():
39+
with patch("platform.system", return_value="Linux"), patch(
40+
"shutil.which", return_value=None
41+
), patch("webbrowser.open") as mock_webbrowser_open:
42+
typer.launch(url)
43+
44+
mock_webbrowser_open.assert_called_once_with(url)
45+
46+
47+
def test_calls_original_launch_when_not_passing_urls():
48+
with patch("typer.main.click.launch", return_value=0) as launch_mock:
49+
typer.launch("not a url")
50+
51+
launch_mock.assert_called_once_with("not a url")

typer/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Typer, build great CLIs. Easy to code. Based on Python type hints."""
22

3-
__version__ = "0.12.5"
3+
__version__ = "0.13.0"
44

55
from shutil import get_terminal_size as get_terminal_size
66

@@ -12,7 +12,6 @@
1212
from click.termui import echo_via_pager as echo_via_pager
1313
from click.termui import edit as edit
1414
from click.termui import getchar as getchar
15-
from click.termui import launch as launch
1615
from click.termui import pause as pause
1716
from click.termui import progressbar as progressbar
1817
from click.termui import prompt as prompt
@@ -28,6 +27,7 @@
2827

2928
from . import colors as colors
3029
from .main import Typer as Typer
30+
from .main import launch as launch
3131
from .main import run as run
3232
from .models import CallbackParam as CallbackParam
3333
from .models import Context as Context

typer/core.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,9 +211,11 @@ def _main(
211211
# even always obvious that `rv` indicates success/failure
212212
# by its truthiness/falsiness
213213
ctx.exit()
214-
except (EOFError, KeyboardInterrupt) as e:
214+
except EOFError as e:
215215
click.echo(file=sys.stderr)
216216
raise click.Abort() from e
217+
except KeyboardInterrupt as e:
218+
raise click.exceptions.Exit(130) from e
217219
except click.ClickException as e:
218220
if not standalone_mode:
219221
raise

typer/main.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import inspect
22
import os
3+
import platform
4+
import shutil
5+
import subprocess
36
import sys
47
import traceback
58
from datetime import datetime
@@ -1090,3 +1093,68 @@ def run(function: Callable[..., Any]) -> None:
10901093
app = Typer(add_completion=False)
10911094
app.command()(function)
10921095
app()
1096+
1097+
1098+
def _is_macos() -> bool:
1099+
return platform.system() == "Darwin"
1100+
1101+
1102+
def _is_linux_or_bsd() -> bool:
1103+
if platform.system() == "Linux":
1104+
return True
1105+
1106+
return "BSD" in platform.system()
1107+
1108+
1109+
def launch(url: str, wait: bool = False, locate: bool = False) -> int:
1110+
"""This function launches the given URL (or filename) in the default
1111+
viewer application for this file type. If this is an executable, it
1112+
might launch the executable in a new session. The return value is
1113+
the exit code of the launched application. Usually, ``0`` indicates
1114+
success.
1115+
1116+
This function handles url in different operating systems separately:
1117+
- On macOS (Darwin), it uses the 'open' command.
1118+
- On Linux and BSD, it uses 'xdg-open' if available.
1119+
- On Windows (and other OSes), it uses the standard webbrowser module.
1120+
1121+
The function avoids, when possible, using the webbrowser module on Linux and macOS
1122+
to prevent spammy terminal messages from some browsers (e.g., Chrome).
1123+
1124+
Examples::
1125+
1126+
typer.launch("https://typer.tiangolo.com/")
1127+
typer.launch("/my/downloaded/file", locate=True)
1128+
1129+
:param url: URL or filename of the thing to launch.
1130+
:param wait: Wait for the program to exit before returning. This
1131+
only works if the launched program blocks. In particular,
1132+
``xdg-open`` on Linux does not block.
1133+
:param locate: if this is set to `True` then instead of launching the
1134+
application associated with the URL it will attempt to
1135+
launch a file manager with the file located. This
1136+
might have weird effects if the URL does not point to
1137+
the filesystem.
1138+
"""
1139+
1140+
if url.startswith("http://") or url.startswith("https://"):
1141+
if _is_macos():
1142+
return subprocess.Popen(
1143+
["open", url], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT
1144+
).wait()
1145+
1146+
has_xdg_open = _is_linux_or_bsd() and shutil.which("xdg-open") is not None
1147+
1148+
if has_xdg_open:
1149+
return subprocess.Popen(
1150+
["xdg-open", url], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT
1151+
).wait()
1152+
1153+
import webbrowser
1154+
1155+
webbrowser.open(url)
1156+
1157+
return 0
1158+
1159+
else:
1160+
return click.launch(url)

0 commit comments

Comments
 (0)