Skip to content

Commit

Permalink
✨ Add support for Click 8 while keeping compatibility with Click 7 (#317
Browse files Browse the repository at this point in the history
)
  • Loading branch information
tiangolo committed Aug 30, 2021
1 parent c3a4c72 commit b972981
Show file tree
Hide file tree
Showing 40 changed files with 1,343 additions and 468 deletions.
9 changes: 8 additions & 1 deletion .github/workflows/test.yml
Expand Up @@ -11,18 +11,25 @@ jobs:
strategy:
matrix:
python-version: [3.6, 3.7, 3.8]
click-7: [true, false]
fail-fast: false

steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v1
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install Flit
run: pip install flit
- name: Install Dependencies
run: flit install --deps=develop --symlink
- name: Install Click 7
if: matrix.click-7
run: pip install "click<8.0.0"
- name: Lint
if: ${{ matrix.python-version != '3.6' && matrix.click-7 == false }}
run: bash scripts/lint.sh
- name: Test
run: bash scripts/test.sh
- name: Upload coverage
Expand Down
12 changes: 1 addition & 11 deletions docs/tutorial/using-click.md
Expand Up @@ -99,17 +99,7 @@ $ python main.py
// Notice we have both subcommands, top and hello
Usage: main.py [OPTIONS] COMMAND [ARGS]...

Typer app, including Click subapp

Options:
--install-completion Install completion for the current shell.
--show-completion Show completion for the current shell, to copy it or customize the installation.

--help Show this message and exit.

Commands:
hello Simple program that greets NAME for a total of COUNT times.
top Top level command, form Typer
Error: Missing command.

// Call the Typer part
$ python main.py top
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Expand Up @@ -27,7 +27,7 @@ classifiers = [
"License :: OSI Approved :: MIT License"
]
requires = [
"click >= 7.1.1, <7.2.0"
"click >= 7.1.1, <9.0.0"
]
description-file = "README.md"
requires-python = ">=3.6"
Expand All @@ -43,7 +43,7 @@ test = [
"coverage >=5.2,<6.0",
"pytest-xdist >=1.32.0,<2.0.0",
"pytest-sugar >=0.9.4,<0.10.0",
"mypy ==0.782",
"mypy ==0.910",
"black >=19.10b0,<20.0b0",
"isort >=5.0.6,<6.0.0"
]
Expand Down
1 change: 1 addition & 0 deletions scripts/get-pwsh-activate.sh
@@ -0,0 +1 @@
curl https://raw.githubusercontent.com/python/cpython/main/Lib/venv/scripts/common/Activate.ps1 -o Activate.ps1
1 change: 0 additions & 1 deletion scripts/test.sh
Expand Up @@ -4,6 +4,5 @@ set -e
set -x

bash ./scripts/test-files.sh
bash ./scripts/lint.sh
# Use xdist-pytest --forked to ensure modified sys.path to import relative modules in examples keeps working
pytest --cov=typer --cov=tests --cov=docs_src --cov-report=term-missing --cov-report=xml -o console_output_style=progress --forked --numprocesses=auto ${@}
Empty file added tests/assets/__init__.py
Empty file.
29 changes: 29 additions & 0 deletions tests/assets/compat_click7_8.py
@@ -0,0 +1,29 @@
from typing import List

import click
import typer

app = typer.Typer()


def shell_complete(
ctx: click.Context, param: click.Parameter, incomplete: str
) -> List[str]:
return ["Jonny"]


@app.command(context_settings={"auto_envvar_prefix": "TEST"})
def main(
name: str = typer.Option("John", hidden=True),
lastname: str = typer.Option("Doe", "/lastname", show_default="Mr. Doe"),
age: int = typer.Option(lambda: 42, show_default=True),
nickname: str = typer.Option("", shell_complete=shell_complete),
):
"""
Say hello.
"""
typer.echo(f"Hello {name} {lastname}, it seems you have {age}, {nickname}")


if __name__ == "__main__":
app()
Empty file added tests/test_compat/__init__.py
Empty file.
41 changes: 41 additions & 0 deletions tests/test_compat/test_option_get_help.py
@@ -0,0 +1,41 @@
import os
import subprocess

from typer.testing import CliRunner

from tests.assets import compat_click7_8 as mod

runner = CliRunner()


def test_hidden_option():
result = runner.invoke(mod.app, ["--help"])
assert result.exit_code == 0
assert "Say hello" in result.output
assert "--name" not in result.output
assert "/lastname" in result.output
assert "TEST_LASTNAME" in result.output
assert "(dynamic)" in result.output


def test_coverage_call():
result = runner.invoke(mod.app)
assert result.exit_code == 0
assert "Hello John Doe, it seems you have 42" in result.output


def test_completion():
result = subprocess.run(
["coverage", "run", mod.__file__, " "],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
encoding="utf-8",
env={
**os.environ,
"_COMPAT_CLICK7_8.PY_COMPLETE": "complete_zsh",
"_TYPER_COMPLETE_ARGS": "compat_click7_8.py --nickname ",
"_TYPER_COMPLETE_TESTING": "True",
},
)
# TODO: when deprecating Click 7, remove second option
assert "Jonny" in result.stdout or "_files" in result.stdout
2 changes: 1 addition & 1 deletion tests/test_completion/test_completion.py
Expand Up @@ -106,7 +106,7 @@ def test_completion_source_invalid_instruction():
"_TYPER_COMPLETE_TESTING": "True",
},
)
assert "Hello World" in result.stdout
assert 'Completion instruction "explode" not supported.' in result.stderr


def test_completion_source_zsh():
Expand Down
6 changes: 5 additions & 1 deletion tests/test_completion/test_completion_install.py
Expand Up @@ -26,7 +26,11 @@ def test_completion_install_no_shell():
"_TYPER_COMPLETE_TEST_DISABLE_SHELL_DETECTION": "True",
},
)
assert "Error: --install-completion option requires an argument" in result.stderr
# TODO: when deprecating Click 7, remove second option
assert (
"Error: Option '--install-completion' requires an argument" in result.stderr
or "Error: --install-completion option requires an argument" in result.stderr
)


def test_completion_install_bash():
Expand Down
6 changes: 5 additions & 1 deletion tests/test_completion/test_completion_show.py
Expand Up @@ -16,7 +16,11 @@ def test_completion_show_no_shell():
"_TYPER_COMPLETE_TEST_DISABLE_SHELL_DETECTION": "True",
},
)
assert "Error: --show-completion option requires an argument" in result.stderr
# TODO: when deprecating Click 7, remove second option
assert (
"Error: Option '--show-completion' requires an argument" in result.stderr
or "Error: --show-completion option requires an argument" in result.stderr
)


def test_completion_show_bash():
Expand Down
20 changes: 15 additions & 5 deletions tests/test_others.py
Expand Up @@ -141,11 +141,14 @@ def test_completion_untyped_parameters():
},
)
assert "info name is: completion_no_types.py" in result.stderr
assert "args is: ['--name', 'Sebastian', '--name']" in result.stderr
# TODO: when deprecating Click 7, remove second option
assert (
"args is: []" in result.stderr
or "args is: ['--name', 'Sebastian', '--name']" in result.stderr
)
assert "incomplete is: Ca" in result.stderr
assert '"Camila":"The reader of books."' in result.stdout
assert '"Carlos":"The writer of scripts."' in result.stdout
assert '"Sebastian":"The type hints guy."' in result.stdout

result = subprocess.run(
["coverage", "run", str(file_path)],
Expand All @@ -171,11 +174,14 @@ def test_completion_untyped_parameters_different_order_correct_names():
},
)
assert "info name is: completion_no_types_order.py" in result.stderr
assert "args is: ['--name', 'Sebastian', '--name']" in result.stderr
# TODO: when deprecating Click 7, remove second option
assert (
"args is: []" in result.stderr
or "args is: ['--name', 'Sebastian', '--name']" in result.stderr
)
assert "incomplete is: Ca" in result.stderr
assert '"Camila":"The reader of books."' in result.stdout
assert '"Carlos":"The writer of scripts."' in result.stdout
assert '"Sebastian":"The type hints guy."' in result.stdout

result = subprocess.run(
["coverage", "run", str(file_path)],
Expand Down Expand Up @@ -213,8 +219,12 @@ def main(arg1, arg2: int, arg3: "int", arg4: bool = False, arg5: "bool" = False)
typer.echo(f"arg5: {type(arg5)} {arg5}")

result = runner.invoke(app, ["Hello", "2", "invalid"])
# TODO: when deprecating Click 7, remove second option

assert (
"Error: Invalid value for 'ARG3': invalid is not a valid integer"
"Error: Invalid value for 'ARG3': 'invalid' is not a valid integer"
in result.stdout
or "Error: Invalid value for 'ARG3': invalid is not a valid integer"
in result.stdout
)
result = runner.invoke(app, ["Hello", "2", "3", "--arg4", "--arg5"])
Expand Down
Expand Up @@ -49,7 +49,11 @@ def test_delete_verbose():
def test_wrong_verbose():
result = runner.invoke(app, ["delete", "--verbose", "Camila"])
assert result.exit_code != 0
assert "Error: no such option: --verbose" in result.output
# TODO: when deprecating Click 7, remove second option
assert (
"Error: No such option: --verbose" in result.output
or "Error: no such option: --verbose" in result.output
)


def test_script():
Expand Down
24 changes: 20 additions & 4 deletions tests/test_tutorial/test_commands/test_help/test_tutorial001.py
Expand Up @@ -66,28 +66,44 @@ def test_create():
def test_delete():
result = runner.invoke(app, ["delete", "Camila"], input="y\n")
assert result.exit_code == 0
assert "Are you sure you want to delete the user? [y/N]:" in result.output
# TODO: when deprecating Click 7, remove second option
assert (
"Are you sure you want to delete the user? [y/n]:" in result.output
or "Are you sure you want to delete the user? [y/N]:" in result.output
)
assert "Deleting user: Camila" in result.output


def test_no_delete():
result = runner.invoke(app, ["delete", "Camila"], input="n\n")
assert result.exit_code == 0
assert "Are you sure you want to delete the user? [y/N]:" in result.output
# TODO: when deprecating Click 7, remove second option
assert (
"Are you sure you want to delete the user? [y/n]:" in result.output
or "Are you sure you want to delete the user? [y/N]:" in result.output
)
assert "Operation cancelled" in result.output


def test_delete_all():
result = runner.invoke(app, ["delete-all"], input="y\n")
assert result.exit_code == 0
assert "Are you sure you want to delete ALL users? [y/N]:" in result.output
# TODO: when deprecating Click 7, remove second option
assert (
"Are you sure you want to delete ALL users? [y/n]:" in result.output
or "Are you sure you want to delete ALL users? [y/N]:" in result.output
)
assert "Deleting all users" in result.output


def test_no_delete_all():
result = runner.invoke(app, ["delete-all"], input="n\n")
assert result.exit_code == 0
assert "Are you sure you want to delete ALL users? [y/N]:" in result.output
# TODO: when deprecating Click 7, remove second option
assert (
"Are you sure you want to delete ALL users? [y/n]:" in result.output
or "Are you sure you want to delete ALL users? [y/N]:" in result.output
)
assert "Operation cancelled" in result.output


Expand Down
30 changes: 25 additions & 5 deletions tests/test_tutorial/test_commands/test_options/test_tutorial001.py
Expand Up @@ -28,35 +28,55 @@ def test_create():
def test_delete():
result = runner.invoke(app, ["delete", "Camila"], input="y\n")
assert result.exit_code == 0
assert "Are you sure you want to delete the user? [y/N]:" in result.output
# TODO: when deprecating Click 7, remove second option
assert (
"Are you sure you want to delete the user? [y/n]:" in result.output
or "Are you sure you want to delete the user? [y/N]:" in result.output
)
assert "Deleting user: Camila" in result.output


def test_no_delete():
result = runner.invoke(app, ["delete", "Camila"], input="n\n")
assert result.exit_code == 0
assert "Are you sure you want to delete the user? [y/N]:" in result.output
# TODO: when deprecating Click 7, remove second option
assert (
"Are you sure you want to delete the user? [y/n]:" in result.output
or "Are you sure you want to delete the user? [y/N]:" in result.output
)
assert "Operation cancelled" in result.output


def test_delete_all():
result = runner.invoke(app, ["delete-all"], input="y\n")
assert result.exit_code == 0
assert "Are you sure you want to delete ALL users? [y/N]:" in result.output
# TODO: when deprecating Click 7, remove second option
assert (
"Are you sure you want to delete ALL users? [y/n]:" in result.output
or "Are you sure you want to delete ALL users? [y/N]:" in result.output
)
assert "Deleting all users" in result.output


def test_no_delete_all():
result = runner.invoke(app, ["delete-all"], input="n\n")
assert result.exit_code == 0
assert "Are you sure you want to delete ALL users? [y/N]:" in result.output
# TODO: when deprecating Click 7, remove second option
assert (
"Are you sure you want to delete ALL users? [y/n]:" in result.output
or "Are you sure you want to delete ALL users? [y/N]:" in result.output
)
assert "Operation cancelled" in result.output


def test_delete_all_force():
result = runner.invoke(app, ["delete-all", "--force"])
assert result.exit_code == 0
assert "Are you sure you want to delete ALL users? [y/N]:" not in result.output
# TODO: when deprecating Click 7, remove second option
assert (
"Are you sure you want to delete ALL users? [y/n]:" not in result.output
or "Are you sure you want to delete ALL users? [y/N]:" not in result.output
)
assert "Deleting all users" in result.output


Expand Down
Expand Up @@ -29,7 +29,12 @@ def test_defaults():
def test_invalid_args():
result = runner.invoke(app, ["Draco", "Hagrid"])
assert result.exit_code != 0
assert "Error: argument names takes 3 values" in result.stdout
# TODO: when deprecating Click 7, remove second option

assert (
"Error: Argument 'names' takes 3 values" in result.stdout
or "Error: argument names takes 3 values" in result.stdout
)


def test_valid_args():
Expand Down
Expand Up @@ -34,7 +34,12 @@ def test_user_2():
def test_invalid_user():
result = runner.invoke(app, ["--user", "Camila", "50"])
assert result.exit_code != 0
assert "Error: --user option requires 3 arguments" in result.output
# TODO: when deprecating Click 7, remove second option

assert (
"Error: Option '--user' requires 3 arguments" in result.output
or "Error: --user option requires 3 arguments" in result.output
)


def test_script():
Expand Down
Expand Up @@ -28,7 +28,8 @@ def test_completion():
assert '"Camila":"The reader of books."' in result.stdout
assert '"Carlos":"The writer of scripts."' in result.stdout
assert '"Sebastian":"The type hints guy."' in result.stdout
assert "['--name']" in result.stderr
# TODO: when deprecating Click 7, remove second option
assert "[]" in result.stderr or "['--name']" in result.stderr


def test_1():
Expand Down
Expand Up @@ -28,7 +28,8 @@ def test_completion():
assert '"Camila":"The reader of books."' in result.stdout
assert '"Carlos":"The writer of scripts."' in result.stdout
assert '"Sebastian":"The type hints guy."' not in result.stdout
assert "['--name', 'Sebastian', '--name']" in result.stderr
# TODO: when deprecating Click 7, remove second option
assert "[]" in result.stderr or "['--name', 'Sebastian', '--name']" in result.stderr


def test_1():
Expand Down

1 comment on commit b972981

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.