Skip to content

⬆️ Add compatibility with Click 8.2 #1222

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
May 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6cd8b4d
♻️ Update internals for compatibility with Click 8.2
tiangolo May 13, 2025
6fd36e5
♻️ Update _completion_classes.py with new Click imports
tiangolo May 13, 2025
cd66e63
👷 Only lint on recent versions of Python (recent Click)
tiangolo May 14, 2025
669e640
✅ Update tests for Click 8.2, with compat for < 8.2
tiangolo May 14, 2025
5144dfd
🐛 Allow passing None as context, Click doesn't type it that way but e…
tiangolo May 14, 2025
c1a0e68
🚨 Tweak linter
tiangolo May 14, 2025
7146c06
✏️ Update typer/core.py
tiangolo May 22, 2025
7fad74e
♻️ Refactor internal implementation with Click compat instructions: h…
tiangolo May 22, 2025
a2c4906
📌 Pin Click to debug CI
tiangolo May 22, 2025
02f6565
Merge branch 'master' into click-8.2
tiangolo May 22, 2025
1cbcbc1
Merge branch 'master' into click-8.2
tiangolo May 22, 2025
bd98584
⏪️ Revert implementation to previous for compat
tiangolo May 22, 2025
8e34fbc
✅ Update tests for compatibility with Click 8.2
tiangolo May 22, 2025
17350f5
🔧 Add .env for local development with VS Code
tiangolo May 22, 2025
5e571b6
✅ Add extra test for enums
tiangolo May 26, 2025
3b8f9bc
♻️ Add custom TyperChoice type for compatibility with Click 8.2.0, ke…
tiangolo May 26, 2025
ce2f27b
🚨 Update types for mypy
tiangolo May 26, 2025
a336edd
✅ Update test for new Click output
tiangolo May 26, 2025
15067a3
📝 Update docs for tests with stderr
tiangolo May 26, 2025
79d38e3
✅ Update tests to use `result.output` instead of `result.stdout`
tiangolo May 26, 2025
7da4979
📌 Remove Click lower bound pin
tiangolo May 26, 2025
5c0b4a6
✅ Add extra test for coverage
tiangolo May 26, 2025
45ca749
✅ Tweak test
tiangolo May 26, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Environment variables automatically read by VS Code, e.g. running tests

# For tests, a large terminal width
TERMINAL_WIDTH=3000
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ jobs:
- name: Install Dependencies
run: uv pip install -r requirements-tests.txt
- name: Lint
if: matrix.python-version != '3.7' && matrix.python-version != '3.8' && matrix.python-version != '3.9'
run: bash scripts/lint.sh
- run: mkdir coverage
- run: bash ./scripts/test-files.sh
Expand Down
2 changes: 1 addition & 1 deletion docs/tutorial/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ Then we check that the text printed to "standard output" contains the text that

/// tip

You could also check output sent to "standard error" (`stderr`) instead of "standard output" (`stdout`). To do so, make sure your `CliRunner` instance is created with the `mix_stderr=False` argument. You need this setting to be able to access `result.stderr` in tests.
You could also check the output sent to "standard error" (`stderr`) or "standard output" (`stdout`) independently by accessing `result.stdout` and `result.stderr` in your tests.

///

Expand Down
4 changes: 2 additions & 2 deletions docs_src/testing/app01/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
def test_app():
result = runner.invoke(app, ["Camila", "--city", "Berlin"])
assert result.exit_code == 0
assert "Hello Camila" in result.stdout
assert "Let's have a coffee in Berlin" in result.stdout
assert "Hello Camila" in result.output
assert "Let's have a coffee in Berlin" in result.output
2 changes: 1 addition & 1 deletion docs_src/testing/app02/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@
def test_app():
result = runner.invoke(app, ["Camila"], input="[email protected]\n")
assert result.exit_code == 0
assert "Hello Camila, your email is: [email protected]" in result.stdout
assert "Hello Camila, your email is: [email protected]" in result.output
2 changes: 1 addition & 1 deletion docs_src/testing/app02_an/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@
def test_app():
result = runner.invoke(app, ["Camila"], input="[email protected]\n")
assert result.exit_code == 0
assert "Hello Camila, your email is: [email protected]" in result.stdout
assert "Hello Camila, your email is: [email protected]" in result.output
2 changes: 1 addition & 1 deletion docs_src/testing/app03/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@
def test_app():
result = runner.invoke(app, ["--name", "Camila"])
assert result.exit_code == 0
assert "Hello Camila" in result.stdout
assert "Hello Camila" in result.output
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ classifiers = [
"License :: OSI Approved :: MIT License",
]
dependencies = [
"click >= 8.0.0,<8.2",
"click >= 8.0.0",
"typing-extensions >= 3.7.4.3",
]
readme = "README.md"
Expand Down
2 changes: 1 addition & 1 deletion tests/test_completion/test_completion_show.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,4 +146,4 @@ def test_completion_show_invalid_shell():
shellingham, "detect_shell", return_value=("xshell", "/usr/bin/xshell")
):
result = runner.invoke(app, ["--show-completion"])
assert "Shell xshell not supported" in result.stdout
assert "Shell xshell not supported" in result.output
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@

def test_no_arg():
result = runner.invoke(app)
assert result.exit_code == 0
assert "[OPTIONS] COMMAND [ARGS]..." in result.output
assert "Commands" in result.output
assert "create" in result.output
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ def test_1():
def test_2():
result = runner.invoke(app, ["--name", "rick"])
assert result.exit_code != 0
assert "Invalid value for '--name': Only Camila is allowed" in result.output
assert "Invalid value for '--name'" in result.output
assert "Only Camila is allowed" in result.output


def test_script():
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ def test_1():
def test_2():
result = runner.invoke(app, ["--name", "rick"])
assert result.exit_code != 0
assert "Invalid value for '--name': Only Camila is allowed" in result.output
assert "Invalid value for '--name'" in result.output
assert "Only Camila is allowed" in result.output


def test_script():
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ def test_1():
def test_2():
result = runner.invoke(app, ["--name", "rick"])
assert result.exit_code != 0
assert "Invalid value for '--name': Only Camila is allowed" in result.output
assert "Invalid value for '--name'" in result.output
assert "Only Camila is allowed" in result.output


def test_script():
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ def test_1():
def test_2():
result = runner.invoke(app, ["--name", "rick"])
assert result.exit_code != 0
assert "Invalid value for '--name': Only Camila is allowed" in result.output
assert "Invalid value for '--name'" in result.output
assert "Only Camila is allowed" in result.output


def test_script():
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ def test_1():
def test_2():
result = runner.invoke(app, ["--name", "rick"])
assert result.exit_code != 0
assert "Invalid value for '--name': Only Camila is allowed" in result.output
assert "Invalid value for '--name'" in result.output
assert "Only Camila is allowed" in result.output


def test_script():
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ def test_1():
def test_2():
result = runner.invoke(app, ["--name", "rick"])
assert result.exit_code != 0
assert "Invalid value for '--name': Only Camila is allowed" in result.output
assert "Invalid value for '--name'" in result.output
assert "Only Camila is allowed" in result.output


def test_script():
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
def test_1():
result = runner.invoke(app, ["Camila"])
assert result.exit_code != 0
assert "Missing option '--lastname'." in result.output
assert "Missing option '--lastname'" in result.output


def test_option_lastname():
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
def test_1():
result = runner.invoke(app, ["Camila"])
assert result.exit_code != 0
assert "Missing option '--lastname'." in result.output
assert "Missing option '--lastname'" in result.output


def test_option_lastname():
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ def test_1():
def test_2():
result = runner.invoke(app, ["--name", "rick"])
assert result.exit_code != 0
assert "Invalid value for '--name': Only Camila is allowed" in result.output
assert "Invalid value for '--name'" in result.output
assert "Only Camila is allowed" in result.output


def test_3():
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ def test_1():
def test_2():
result = runner.invoke(app, ["--name", "rick"])
assert result.exit_code != 0
assert "Invalid value for '--name': Only Camila is allowed" in result.output
assert "Invalid value for '--name'" in result.output
assert "Only Camila is allowed" in result.output


def test_3():
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,17 @@ def test_main():
assert "Training neural network of type: conv" in result.output


def test_main_default():
result = runner.invoke(app)
assert result.exit_code == 0
assert "Training neural network of type: simple" in result.output


def test_invalid_case():
result = runner.invoke(app, ["--network", "CONV"])
assert result.exit_code != 0
assert "Invalid value for '--network': 'CONV' is not one of" in result.output
assert "Invalid value for '--network'" in result.output
assert "'CONV' is not one of" in result.output
assert "simple" in result.output
assert "conv" in result.output
assert "lstm" in result.output
Expand All @@ -38,7 +45,8 @@ def test_invalid_case():
def test_invalid_other():
result = runner.invoke(app, ["--network", "capsule"])
assert result.exit_code != 0
assert "Invalid value for '--network': 'capsule' is not one of" in result.output
assert "Invalid value for '--network'" in result.output
assert "'capsule' is not one of" in result.output
assert "simple" in result.output
assert "conv" in result.output
assert "lstm" in result.output
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ def test_params():
def test_invalid():
result = runner.invoke(app, ["Camila", "--age", "15.3"])
assert result.exit_code != 0
assert "Invalid value for '--age': '15.3' is not a valid integer" in result.output
assert "Invalid value for '--age'" in result.output
assert "'15.3' is not a valid integer" in result.output


def test_script():
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,15 @@ def test_invalid_id():
def test_invalid_age():
result = runner.invoke(app, ["5", "--age", "15"])
assert result.exit_code != 0
assert "Invalid value for '--age': 15 is not in the range x>=18" in result.output
assert "Invalid value for '--age'" in result.output
assert "15 is not in the range x>=18" in result.output


def test_invalid_score():
result = runner.invoke(app, ["5", "--age", "20", "--score", "100.5"])
assert result.exit_code != 0
assert (
"Invalid value for '--score': 100.5 is not in the range x<=100."
in result.output
)
assert "Invalid value for '--score'" in result.output
assert "100.5 is not in the range x<=100." in result.output


def test_negative_score():
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,15 @@ def test_invalid_id():
def test_invalid_age():
result = runner.invoke(app, ["5", "--age", "15"])
assert result.exit_code != 0
assert "Invalid value for '--age': 15 is not in the range x>=18" in result.output
assert "Invalid value for '--age'" in result.output
assert "15 is not in the range x>=18" in result.output


def test_invalid_score():
result = runner.invoke(app, ["5", "--age", "20", "--score", "100.5"])
assert result.exit_code != 0
assert (
"Invalid value for '--score': 100.5 is not in the range x<=100."
in result.output
)
assert "Invalid value for '--score'" in result.output
assert "100.5 is not in the range x<=100." in result.output


def test_negative_score():
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ def test_not_exists(tmpdir):
config_file.unlink()
result = runner.invoke(app, ["--config", f"{config_file}"])
assert result.exit_code != 0
assert "Invalid value for '--config': File" in result.output
assert "Invalid value for '--config'" in result.output
assert "File" in result.output
assert "does not exist" in result.output


Expand All @@ -35,7 +36,8 @@ def test_exists(tmpdir):
def test_dir():
result = runner.invoke(app, ["--config", "./"])
assert result.exit_code != 0
assert "Invalid value for '--config': File './' is a directory." in result.output
assert "Invalid value for '--config'" in result.output
assert "File './' is a directory." in result.output


def test_script():
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ def test_not_exists(tmpdir):
config_file.unlink()
result = runner.invoke(app, ["--config", f"{config_file}"])
assert result.exit_code != 0
assert "Invalid value for '--config': File" in result.output
assert "Invalid value for '--config'" in result.output
assert "File" in result.output
assert "does not exist" in result.output


Expand All @@ -35,7 +36,8 @@ def test_exists(tmpdir):
def test_dir():
result = runner.invoke(app, ["--config", "./"])
assert result.exit_code != 0
assert "Invalid value for '--config': File './' is a directory." in result.output
assert "Invalid value for '--config'" in result.output
assert "File './' is a directory." in result.output


def test_script():
Expand Down
34 changes: 34 additions & 0 deletions tests/test_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from enum import Enum

import typer
from typer.testing import CliRunner

app = typer.Typer(context_settings={"token_normalize_func": str.lower})


class User(str, Enum):
rick = "Rick"
morty = "Morty"


@app.command()
def hello(name: User = User.rick) -> None:
print(f"Hello {name.value}!")


runner = CliRunner()


def test_enum_choice() -> None:
# This test is only for coverage of the new custom TyperChoice class
result = runner.invoke(app, ["--name", "morty"], catch_exceptions=False)
assert result.exit_code == 0
assert "Hello Morty!" in result.output

result = runner.invoke(app, ["--name", "Rick"])
assert result.exit_code == 0
assert "Hello Rick!" in result.output

result = runner.invoke(app, ["--name", "RICK"])
assert result.exit_code == 0
assert "Hello Rick!" in result.output
16 changes: 12 additions & 4 deletions typer/_completion_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@
Shells,
)

try:
from click.shell_completion import split_arg_string as click_split_arg_string
except ImportError: # pragma: no cover
# TODO: when removing support for Click < 8.2, remove this import
from click.parser import ( # type: ignore[no-redef]
split_arg_string as click_split_arg_string,
)

try:
import shellingham
except ImportError: # pragma: no cover
Expand Down Expand Up @@ -43,7 +51,7 @@ def source_vars(self) -> Dict[str, Any]:
}

def get_completion_args(self) -> Tuple[List[str], str]:
cwords = click.parser.split_arg_string(os.environ["COMP_WORDS"])
cwords = click_split_arg_string(os.environ["COMP_WORDS"])
cword = int(os.environ["COMP_CWORD"])
args = cwords[1:cword]

Expand Down Expand Up @@ -80,7 +88,7 @@ def source_vars(self) -> Dict[str, Any]:

def get_completion_args(self) -> Tuple[List[str], str]:
completion_args = os.getenv("_TYPER_COMPLETE_ARGS", "")
cwords = click.parser.split_arg_string(completion_args)
cwords = click_split_arg_string(completion_args)
args = cwords[1:]
if args and not completion_args.endswith(" "):
incomplete = args[-1]
Expand Down Expand Up @@ -131,7 +139,7 @@ def source_vars(self) -> Dict[str, Any]:

def get_completion_args(self) -> Tuple[List[str], str]:
completion_args = os.getenv("_TYPER_COMPLETE_ARGS", "")
cwords = click.parser.split_arg_string(completion_args)
cwords = click_split_arg_string(completion_args)
args = cwords[1:]
if args and not completion_args.endswith(" "):
incomplete = args[-1]
Expand Down Expand Up @@ -185,7 +193,7 @@ def source_vars(self) -> Dict[str, Any]:
def get_completion_args(self) -> Tuple[List[str], str]:
completion_args = os.getenv("_TYPER_COMPLETE_ARGS", "")
incomplete = os.getenv("_TYPER_COMPLETE_WORD_TO_COMPLETE", "")
cwords = click.parser.split_arg_string(completion_args)
cwords = click_split_arg_string(completion_args)
args = cwords[1:-1] if incomplete else cwords[1:]
return args, incomplete

Expand Down
Loading
Loading