diff --git a/.gitignore b/.gitignore index bd4cbf4..157036c 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ dist site .coverage htmlcov +.pytest_cache diff --git a/README.md b/README.md index cc6b2c4..272d16d 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Typer is library to build Completion everywhere. Less time debugging. Designed to be easy to use and learn. Less time reading docs. -* **Easy to use**: It's easy to use for the final users. Automatic help, and (optional) automatic completion for all shells. +* **Easy to use**: It's easy to use for the final users. Automatic help, and automatic completion for all shells. * **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs. * **Start simple**: The simplest example adds only 2 lines of code to your app: **1 import, 1 function call**. * **Grow large**: Grow in complexity as much as you want, create arbitrarily complex trees of commands and groups of subcommands, with options and arguments. @@ -243,7 +243,7 @@ And similarly for **files**, **paths**, **enums** (choices), etc. And there are **You get**: great editor support, including **completion** and **type checks** everywhere. -**Your users get**: automatic **`--help`**, (optional) **auto completion** in their terminal (Bash, Zsh, Fish, PowerShell) when they install your package or when using Typer CLI. +**Your users get**: automatic **`--help`**, **auto completion** in their terminal (Bash, Zsh, Fish, PowerShell) when they install your package or when using Typer CLI. For a more complete example including more features, see the Tutorial - User Guide. @@ -256,9 +256,11 @@ But you can also install extras: * colorama: and Click will automatically use it to make sure your terminal's colors always work correctly, even in Windows. * Then you can use any tool you want to output your terminal's colors in all the systems, including the integrated `typer.style()` and `typer.secho()` (provided by Click). * Or any other tool, e.g. wasabi, blessings. -* click-completion: and Typer will automatically configure it to provide completion for all the shells, including installation commands. +* shellingham: and Typer will automatically detect the current shell when installing completion. + * With `shellingham` you can just use `--install-completion`. + * Without `shellingham`, you have to pass a *CLI Option value* with the name of the shell to install completion, e.g. `--install-completion bash`. -You can install `typer` with `colorama` and `click-completion` with `pip install typer[all]`. +You can install `typer` with `colorama` and `shellingham` with `pip install typer[all]`. ## Other tools and plug-ins diff --git a/docs/alternatives.md b/docs/alternatives.md index 7a4bd98..2efc136 100644 --- a/docs/alternatives.md +++ b/docs/alternatives.md @@ -63,6 +63,17 @@ It was built with some great ideas and design using the features available in th As someone pointed out: "Nice to see it is built on Click but adds the type stuff. Me gusta!" +### `click-completion` + +`click-completion` is a plug-in for Click. It was created to extend completion support for shells when Click only had support for Bash completion. + +Previous versions of **Typer** had deep integrations with `click-completion` and used it as an optional dependency. But now all the completion logic is implemented internally in **Typer** itself, the internal logic was heavily inspired and using some parts of `click-completion`. + +And now **Typer** improved it to have new features, tests, some bug fixes (for issues in plain `click-completion` and Click), and better support for shells, including modern versions of PowerShell (e.g. the default versions that come with Windows 10). + +!!! check "Inspired **Typer** to" + Provide auto completion for all the shells. + ### FastAPI I created **FastAPI** to provide an easy way to build APIs with autocompletion for everything in the code (and some other features). diff --git a/docs/features.md b/docs/features.md index 9b1acd9..a48fd62 100644 --- a/docs/features.md +++ b/docs/features.md @@ -42,21 +42,31 @@ But by default, it all **"just works"**. The resulting CLI apps created with **Typer** have the nice features of many "pro" command line programs you probably already love. -* Automatic help options for the main CLI program and all the its subcommands. +* Automatic help options for the main CLI program and all its subcommands. * Automatic command and subcommand structure handling (you will see more about subcommands in the Tutorial - User Guide). * Automatic completion for the CLI app in all operating systems, in all the shells (Bash, Zsh, Fish, PowerShell), so that the final user of your app can just hit TAB and get the available options or subcommands. * !!! note "* Auto completion" - For the autocompletion to work on all shells you also need to add the dependency `click-completion`. + Auto completion works when you create a package (installable with `pip`). Or when using [Typer CLI](typer-cli.md){.internal-link target=_blank}. - If **Typer** detects `click-completion` installed, it will automatically create 2 *CLI options*: + If you also add `shellingham` as a dependency, **Typer** will use it to auto-detect the current shell when installing completion. + + **Typer** will automatically create 2 *CLI options*: * `--install-completion`: Install completion for the current shell. - * `--show-completion`: Show completion for the current shell, to copy it or customize the installation. + * `--show-completion`: Show completion for the current shell, to copy it or customize the installation. - Then you can tell the user to run that command and the rest will just work. + If you didn't add `shellingham` those *CLI Options* take a parameter with the name of the shell to install completion for, e.g.: + + * `--install-completion bash`. + * `--show-completion powershell`. - It works when you create a package (installable with `pip`). Or when using [Typer CLI](typer-cli.md){.internal-link target=_blank}. + Then you can tell the user to install completion after installing your CLI program and the rest will just work. + +!!! tip + **Typer**'s completion is implemented internally, it uses ideas and components from Click and ideas from `click-completion`, but it doesn't use `click-completion` internally. + + Then it extends those ideas with features and bug fixes. For example, **Typer** programs also support modern versions of PowerShell (e.g. in Windows 10) among all the other shells. ## The power of Click diff --git a/docs/index.md b/docs/index.md index cc6b2c4..272d16d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -31,7 +31,7 @@ Typer is library to build Completion everywhere. Less time debugging. Designed to be easy to use and learn. Less time reading docs. -* **Easy to use**: It's easy to use for the final users. Automatic help, and (optional) automatic completion for all shells. +* **Easy to use**: It's easy to use for the final users. Automatic help, and automatic completion for all shells. * **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs. * **Start simple**: The simplest example adds only 2 lines of code to your app: **1 import, 1 function call**. * **Grow large**: Grow in complexity as much as you want, create arbitrarily complex trees of commands and groups of subcommands, with options and arguments. @@ -243,7 +243,7 @@ And similarly for **files**, **paths**, **enums** (choices), etc. And there are **You get**: great editor support, including **completion** and **type checks** everywhere. -**Your users get**: automatic **`--help`**, (optional) **auto completion** in their terminal (Bash, Zsh, Fish, PowerShell) when they install your package or when using Typer CLI. +**Your users get**: automatic **`--help`**, **auto completion** in their terminal (Bash, Zsh, Fish, PowerShell) when they install your package or when using Typer CLI. For a more complete example including more features, see the Tutorial - User Guide. @@ -256,9 +256,11 @@ But you can also install extras: * colorama: and Click will automatically use it to make sure your terminal's colors always work correctly, even in Windows. * Then you can use any tool you want to output your terminal's colors in all the systems, including the integrated `typer.style()` and `typer.secho()` (provided by Click). * Or any other tool, e.g. wasabi, blessings. -* click-completion: and Typer will automatically configure it to provide completion for all the shells, including installation commands. +* shellingham: and Typer will automatically detect the current shell when installing completion. + * With `shellingham` you can just use `--install-completion`. + * Without `shellingham`, you have to pass a *CLI Option value* with the name of the shell to install completion, e.g. `--install-completion bash`. -You can install `typer` with `colorama` and `click-completion` with `pip install typer[all]`. +You can install `typer` with `colorama` and `shellingham` with `pip install typer[all]`. ## Other tools and plug-ins diff --git a/mypy.ini b/mypy.ini index 976ba02..9b9c868 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,2 +1,6 @@ [mypy] ignore_missing_imports = True +disallow_untyped_defs = True + +[mypy-tests.*] +disallow_untyped_defs = False diff --git a/pyproject.toml b/pyproject.toml index 2d85209..4405fe5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ Documentation = "https://typer.tiangolo.com/" [tool.flit.metadata.requires-extra] test = [ - "click-completion", + "shellingham", "pytest >=4.4.0", "pytest-cov", "coverage", @@ -54,5 +54,5 @@ doc = [ ] all = [ "colorama", - "click-completion" + "shellingham" ] diff --git a/scripts/lint.sh b/scripts/lint.sh index bf74075..c85f087 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -3,6 +3,6 @@ set -e set -x -mypy typer --disallow-untyped-defs +mypy typer black typer tests --check isort --multi-line=3 --trailing-comma --force-grid-wrap=0 --combine-as --line-width 88 --recursive --check-only --thirdparty typer typer tests diff --git a/tests/test_completion.py b/tests/test_completion.py deleted file mode 100644 index 6da2d9c..0000000 --- a/tests/test_completion.py +++ /dev/null @@ -1,51 +0,0 @@ -import os -import subprocess -import sys -from pathlib import Path - -import typer -from typer.testing import CliRunner - -from first_steps import tutorial001 as mod - -runner = CliRunner() -app = typer.Typer() -app.command()(mod.main) - - -def test_show_completion(): - result = subprocess.run( - [ - "bash", - "-c", - f"{sys.executable} -m coverage run {mod.__file__} --show-completion", - ], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - encoding="utf-8", - env={**os.environ, "SHELL": "/bin/bash"}, - ) - assert "_TUTORIAL001.PY_COMPLETE=complete-bash" in result.stdout - - -def test_install_completion(): - bash_completion_path: Path = Path.home() / ".bash_completion" - text = "" - if bash_completion_path.is_file(): - text = bash_completion_path.read_text() - result = subprocess.run( - [ - "bash", - "-c", - f"{sys.executable} -m coverage run {mod.__file__} --install-completion", - ], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - encoding="utf-8", - env={**os.environ, "SHELL": "/bin/bash"}, - ) - new_text = bash_completion_path.read_text() - bash_completion_path.write_text(text) - assert "_TUTORIAL001.PY_COMPLETE=complete-bash" in new_text - assert "completion installed in" in result.stdout - assert "Completion will take effect once you restart the terminal." in result.stdout diff --git a/tests/test_completion/__init__.py b/tests/test_completion/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_completion/test_completion.py b/tests/test_completion/test_completion.py new file mode 100644 index 0000000..ba48885 --- /dev/null +++ b/tests/test_completion/test_completion.py @@ -0,0 +1,174 @@ +import os +import subprocess +import sys +from pathlib import Path + +from first_steps import tutorial001 as mod + + +def test_show_completion(): + result = subprocess.run( + [ + "bash", + "-c", + f"{sys.executable} -m coverage run {mod.__file__} --show-completion", + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + env={**os.environ, "SHELL": "/bin/bash", "_TYPER_COMPLETE_TESTING": "True",}, + ) + assert "_TUTORIAL001.PY_COMPLETE=complete_bash" in result.stdout + + +def test_install_completion(): + bash_completion_path: Path = Path.home() / ".bash_completion" + text = "" + if bash_completion_path.is_file(): + text = bash_completion_path.read_text() + result = subprocess.run( + [ + "bash", + "-c", + f"{sys.executable} -m coverage run {mod.__file__} --install-completion", + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + env={**os.environ, "SHELL": "/bin/bash", "_TYPER_COMPLETE_TESTING": "True",}, + ) + new_text = bash_completion_path.read_text() + bash_completion_path.write_text(text) + assert 'eval "$(_TUTORIAL001.PY_COMPLETE=source_bash tutorial001.py)' in new_text + assert "completion installed in" in result.stdout + assert "Completion will take effect once you restart the terminal." in result.stdout + + +def test_completion_invalid_instruction(): + result = subprocess.run( + ["coverage", "run", mod.__file__], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + env={ + **os.environ, + "_TUTORIAL001.PY_COMPLETE": "sourcebash", + "_TYPER_COMPLETE_TESTING": "True", + }, + ) + assert result.returncode != 0 + assert "Invalid completion instruction." in result.stderr + + +def test_completion_source_bash(): + result = subprocess.run( + ["coverage", "run", mod.__file__], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + env={ + **os.environ, + "_TUTORIAL001.PY_COMPLETE": "source_bash", + "_TYPER_COMPLETE_TESTING": "True", + }, + ) + assert ( + "complete -o default -F _tutorial001py_completion tutorial001.py" + in result.stdout + ) + + +def test_completion_source_invalid_shell(): + result = subprocess.run( + ["coverage", "run", mod.__file__], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + env={ + **os.environ, + "_TUTORIAL001.PY_COMPLETE": "source_xxx", + "_TYPER_COMPLETE_TESTING": "True", + }, + ) + assert "Shell xxx not supported." in result.stderr + + +def test_completion_source_invalid_instruction(): + result = subprocess.run( + ["coverage", "run", mod.__file__], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + env={ + **os.environ, + "_TUTORIAL001.PY_COMPLETE": "explode_bash", + "_TYPER_COMPLETE_TESTING": "True", + }, + ) + assert "Hello World" in result.stdout + + +def test_completion_source_zsh(): + result = subprocess.run( + ["coverage", "run", mod.__file__], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + env={ + **os.environ, + "_TUTORIAL001.PY_COMPLETE": "source_zsh", + "_TYPER_COMPLETE_TESTING": "True", + }, + ) + assert "compdef _tutorial001py_completion tutorial001.py" in result.stdout + + +def test_completion_source_fish(): + result = subprocess.run( + ["coverage", "run", mod.__file__], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + env={ + **os.environ, + "_TUTORIAL001.PY_COMPLETE": "source_fish", + "_TYPER_COMPLETE_TESTING": "True", + }, + ) + assert "complete --command tutorial001.py --no-files" in result.stdout + + +def test_completion_source_powershell(): + result = subprocess.run( + ["coverage", "run", mod.__file__], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + env={ + **os.environ, + "_TUTORIAL001.PY_COMPLETE": "source_powershell", + "_TYPER_COMPLETE_TESTING": "True", + }, + ) + assert ( + "Register-ArgumentCompleter -Native -CommandName tutorial001.py -ScriptBlock $scriptblock" + in result.stdout + ) + + +def test_completion_source_pwsh(): + result = subprocess.run( + ["coverage", "run", mod.__file__], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + env={ + **os.environ, + "_TUTORIAL001.PY_COMPLETE": "source_pwsh", + "_TYPER_COMPLETE_TESTING": "True", + }, + ) + assert ( + "Register-ArgumentCompleter -Native -CommandName tutorial001.py -ScriptBlock $scriptblock" + in result.stdout + ) diff --git a/tests/test_completion/test_completion_complete.py b/tests/test_completion/test_completion_complete.py new file mode 100644 index 0000000..b6ca2c1 --- /dev/null +++ b/tests/test_completion/test_completion_complete.py @@ -0,0 +1,178 @@ +import os +import subprocess +from commands.help import tutorial001 as mod + + +def test_completion_complete_subcommand_bash(): + result = subprocess.run( + ["coverage", "run", mod.__file__, " "], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + env={ + **os.environ, + "_TUTORIAL001.PY_COMPLETE": "complete_bash", + "COMP_WORDS": "tutorial001.py del", + "COMP_CWORD": "1", + "_TYPER_COMPLETE_TESTING": "True", + }, + ) + assert "delete\ndelete-all" in result.stdout + + +def test_completion_complete_subcommand_bash_invalid(): + result = subprocess.run( + ["coverage", "run", mod.__file__, " "], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + env={ + **os.environ, + "_TUTORIAL001.PY_COMPLETE": "complete_bash", + "COMP_WORDS": "tutorial001.py del", + "COMP_CWORD": "42", + "_TYPER_COMPLETE_TESTING": "True", + }, + ) + assert "create\ndelete\ndelete-all\ninit" in result.stdout + + +def test_completion_complete_subcommand_zsh(): + result = subprocess.run( + ["coverage", "run", mod.__file__, " "], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + env={ + **os.environ, + "_TUTORIAL001.PY_COMPLETE": "complete_zsh", + "_TYPER_COMPLETE_ARGS": "tutorial001.py del", + "_TYPER_COMPLETE_TESTING": "True", + }, + ) + assert ( + """_arguments '*: :(("delete":"Delete a user with USERNAME."\n""" + """\"delete-all":"Delete ALL users in the database."))'""" + ) in result.stdout + + +def test_completion_complete_subcommand_zsh_files(): + result = subprocess.run( + ["coverage", "run", mod.__file__, " "], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + env={ + **os.environ, + "_TUTORIAL001.PY_COMPLETE": "complete_zsh", + "_TYPER_COMPLETE_ARGS": "tutorial001.py delete ", + "_TYPER_COMPLETE_TESTING": "True", + }, + ) + assert ("_files") in result.stdout + + +def test_completion_complete_subcommand_fish(): + result = subprocess.run( + ["coverage", "run", mod.__file__, " "], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + env={ + **os.environ, + "_TUTORIAL001.PY_COMPLETE": "complete_fish", + "_TYPER_COMPLETE_ARGS": "tutorial001.py del", + "_TYPER_COMPLETE_FISH_ACTION": "get-args", + "_TYPER_COMPLETE_TESTING": "True", + }, + ) + assert ( + "delete\tDelete a user with USERNAME.\ndelete-all\tDelete ALL users in the database." + in result.stdout + ) + + +def test_completion_complete_subcommand_fish_should_complete(): + result = subprocess.run( + ["coverage", "run", mod.__file__, " "], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + env={ + **os.environ, + "_TUTORIAL001.PY_COMPLETE": "complete_fish", + "_TYPER_COMPLETE_ARGS": "tutorial001.py del", + "_TYPER_COMPLETE_FISH_ACTION": "is-args", + "_TYPER_COMPLETE_TESTING": "True", + }, + ) + assert result.returncode == 0 + + +def test_completion_complete_subcommand_fish_should_complete_no(): + result = subprocess.run( + ["coverage", "run", mod.__file__, " "], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + env={ + **os.environ, + "_TUTORIAL001.PY_COMPLETE": "complete_fish", + "_TYPER_COMPLETE_ARGS": "tutorial001.py delete ", + "_TYPER_COMPLETE_FISH_ACTION": "is-args", + "_TYPER_COMPLETE_TESTING": "True", + }, + ) + assert result.returncode != 0 + + +def test_completion_complete_subcommand_powershell(): + result = subprocess.run( + ["coverage", "run", mod.__file__, " "], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + env={ + **os.environ, + "_TUTORIAL001.PY_COMPLETE": "complete_powershell", + "_TYPER_COMPLETE_ARGS": "tutorial001.py del", + "_TYPER_COMPLETE_TESTING": "True", + }, + ) + assert ( + "delete:::Delete a user with USERNAME.\ndelete-all:::Delete ALL users in the database." + ) in result.stdout + + +def test_completion_complete_subcommand_pwsh(): + result = subprocess.run( + ["coverage", "run", mod.__file__, " "], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + env={ + **os.environ, + "_TUTORIAL001.PY_COMPLETE": "complete_pwsh", + "_TYPER_COMPLETE_ARGS": "tutorial001.py del", + "_TYPER_COMPLETE_TESTING": "True", + }, + ) + assert ( + "delete:::Delete a user with USERNAME.\ndelete-all:::Delete ALL users in the database." + ) in result.stdout + + +def test_completion_complete_subcommand_noshell(): + result = subprocess.run( + ["coverage", "run", mod.__file__, " "], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + env={ + **os.environ, + "_TUTORIAL001.PY_COMPLETE": "complete_noshell", + "_TYPER_COMPLETE_ARGS": "tutorial001.py del", + "_TYPER_COMPLETE_TESTING": "True", + }, + ) + assert ("") in result.stdout diff --git a/tests/test_completion/test_completion_complete_no_help.py b/tests/test_completion/test_completion_complete_no_help.py new file mode 100644 index 0000000..810a426 --- /dev/null +++ b/tests/test_completion/test_completion_complete_no_help.py @@ -0,0 +1,69 @@ +import os +import subprocess +from commands.index import tutorial002 as mod + + +def test_completion_complete_subcommand_zsh(): + result = subprocess.run( + ["coverage", "run", mod.__file__, " "], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + env={ + **os.environ, + "_TUTORIAL002.PY_COMPLETE": "complete_zsh", + "_TYPER_COMPLETE_ARGS": "tutorial002.py ", + "_TYPER_COMPLETE_TESTING": "True", + }, + ) + assert "create" in result.stdout + assert "delete" in result.stdout + + +def test_completion_complete_subcommand_fish(): + result = subprocess.run( + ["coverage", "run", mod.__file__, " "], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + env={ + **os.environ, + "_TUTORIAL002.PY_COMPLETE": "complete_fish", + "_TYPER_COMPLETE_ARGS": "tutorial002.py ", + "_TYPER_COMPLETE_FISH_ACTION": "get-args", + "_TYPER_COMPLETE_TESTING": "True", + }, + ) + assert "create\ndelete" in result.stdout + + +def test_completion_complete_subcommand_powershell(): + result = subprocess.run( + ["coverage", "run", mod.__file__, " "], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + env={ + **os.environ, + "_TUTORIAL002.PY_COMPLETE": "complete_powershell", + "_TYPER_COMPLETE_ARGS": "tutorial002.py ", + "_TYPER_COMPLETE_TESTING": "True", + }, + ) + assert ("create::: \ndelete::: ") in result.stdout + + +def test_completion_complete_subcommand_pwsh(): + result = subprocess.run( + ["coverage", "run", mod.__file__, " "], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + env={ + **os.environ, + "_TUTORIAL002.PY_COMPLETE": "complete_pwsh", + "_TYPER_COMPLETE_ARGS": "tutorial002.py ", + "_TYPER_COMPLETE_TESTING": "True", + }, + ) + assert ("create::: \ndelete::: ") in result.stdout diff --git a/tests/test_completion/test_completion_install.py b/tests/test_completion/test_completion_install.py new file mode 100644 index 0000000..c9463df --- /dev/null +++ b/tests/test_completion/test_completion_install.py @@ -0,0 +1,140 @@ +import os +import subprocess +from pathlib import Path +from unittest import mock + +import shellingham +import typer +from typer.testing import CliRunner + +from first_steps import tutorial001 as mod + +runner = CliRunner() +app = typer.Typer() +app.command()(mod.main) + + +def test_completion_install_no_shell(): + result = subprocess.run( + ["coverage", "run", mod.__file__, "--install-completion"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + env={ + **os.environ, + "_TYPER_COMPLETE_TESTING": "True", + "_TYPER_COMPLETE_TEST_DISABLE_SHELL_DETECTION": "True", + }, + ) + assert "Error: --install-completion option requires an argument" in result.stderr + + +def test_completion_install_bash(): + bash_completion_path: Path = Path.home() / ".bash_completion" + text = "" + if bash_completion_path.is_file(): + text = bash_completion_path.read_text() + result = subprocess.run( + ["coverage", "run", mod.__file__, "--install-completion", "bash"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + env={ + **os.environ, + "_TYPER_COMPLETE_TESTING": "True", + "_TYPER_COMPLETE_TEST_DISABLE_SHELL_DETECTION": "True", + }, + ) + new_text = bash_completion_path.read_text() + bash_completion_path.write_text(text) + install_script = 'eval "$(_TUTORIAL001.PY_COMPLETE=source_bash tutorial001.py)' + assert install_script not in text + assert install_script in new_text + assert "completion installed in" in result.stdout + assert "Completion will take effect once you restart the terminal." in result.stdout + + +def test_completion_install_zsh(): + completion_path: Path = Path.home() / ".zshrc" + text = "" + if completion_path.is_file(): + text = completion_path.read_text() + result = subprocess.run( + ["coverage", "run", mod.__file__, "--install-completion", "zsh"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + env={ + **os.environ, + "_TYPER_COMPLETE_TESTING": "True", + "_TYPER_COMPLETE_TEST_DISABLE_SHELL_DETECTION": "True", + }, + ) + new_text = completion_path.read_text() + completion_path.write_text(text) + install_script = 'eval "$(_TUTORIAL001.PY_COMPLETE=source_szh tutorial001.py)"' + assert install_script not in text + assert install_script in new_text + assert "completion installed in" in result.stdout + assert "Completion will take effect once you restart the terminal." in result.stdout + + +def test_completion_install_fish(): + script_path = Path(mod.__file__) + completion_path: Path = Path.home() / f".config/fish/completions/{script_path.name}.fish" + text = "" + if completion_path.is_file(): + text = completion_path.read_text() + result = subprocess.run( + ["coverage", "run", mod.__file__, "--install-completion", "fish"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + env={ + **os.environ, + "_TYPER_COMPLETE_TESTING": "True", + "_TYPER_COMPLETE_TEST_DISABLE_SHELL_DETECTION": "True", + }, + ) + new_text = completion_path.read_text() + completion_path.write_text(text) + install_script = "eval (env _TUTORIAL001.PY_COMPLETE=source_fish tutorial001.py)" + assert install_script not in text + assert install_script in new_text + assert "completion installed in" in result.stdout + assert "Completion will take effect once you restart the terminal." in result.stdout + + +runner = CliRunner() +app = typer.Typer() +app.command()(mod.main) + + +def test_completion_install_powershell(): + completion_path: Path = Path.home() / f".config/powershell/Microsoft.PowerShell_profile.ps1" + completion_path_bytes = f"{completion_path}\n".encode("windows-1252") + text = "" + if completion_path.is_file(): + text = completion_path.read_text() + + with mock.patch.object( + shellingham, "detect_shell", return_value=("pwsh", "/usr/bin/pwsh") + ): + with mock.patch.object( + subprocess, + "run", + return_value=subprocess.CompletedProcess( + ["pwsh"], returncode=0, stdout=completion_path_bytes, + ), + ): + result = runner.invoke(app, ["--install-completion"]) + install_script = "Register-ArgumentCompleter -Native -CommandName typer -ScriptBlock $scriptblock" + parent: Path = completion_path.parent + parent.mkdir(parents=True, exist_ok=True) + completion_path.write_text(install_script) + new_text = completion_path.read_text() + completion_path.write_text(text) + assert install_script not in text + assert install_script in new_text + assert "completion installed in" in result.stdout + assert "Completion will take effect once you restart the terminal." in result.stdout diff --git a/tests/test_completion/test_completion_install_source.py b/tests/test_completion/test_completion_install_source.py new file mode 100644 index 0000000..dba58e5 --- /dev/null +++ b/tests/test_completion/test_completion_install_source.py @@ -0,0 +1,107 @@ +import os +import subprocess + +from first_steps import tutorial001 as mod + + +def test_completion_install_source_bash(): + result = subprocess.run( + ["coverage", "run", mod.__file__], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + env={ + **os.environ, + "_TUTORIAL001.PY_COMPLETE": "install-source_bash", + "_TYPER_COMPLETE_TESTING": "True", + }, + ) + assert ( + 'eval "$(_TUTORIAL001.PY_COMPLETE=source_bash tutorial001.py)"' in result.stdout + ) + + +def test_completion_install_source_zsh(): + result = subprocess.run( + ["coverage", "run", mod.__file__], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + env={ + **os.environ, + "_TUTORIAL001.PY_COMPLETE": "install-source_zsh", + "_TYPER_COMPLETE_TESTING": "True", + }, + ) + assert ( + 'eval "$(_TUTORIAL001.PY_COMPLETE=source_szh tutorial001.py)"' in result.stdout + ) + + +def test_completion_install_source_fish(): + result = subprocess.run( + ["coverage", "run", mod.__file__], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + env={ + **os.environ, + "_TUTORIAL001.PY_COMPLETE": "install-source_fish", + "_TYPER_COMPLETE_TESTING": "True", + }, + ) + assert ( + "eval (env _TUTORIAL001.PY_COMPLETE=source_fish tutorial001.py)" + in result.stdout + ) + + +def test_completion_install_source_powershell(): + result = subprocess.run( + ["coverage", "run", mod.__file__], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + env={ + **os.environ, + "_TUTORIAL001.PY_COMPLETE": "install-source_powershell", + "_TYPER_COMPLETE_TESTING": "True", + }, + ) + assert ( + "Register-ArgumentCompleter -Native -CommandName tutorial001.py -ScriptBlock $scriptblock" + in result.stdout + ) + + +def test_completion_install_source_pwsh(): + result = subprocess.run( + ["coverage", "run", mod.__file__], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + env={ + **os.environ, + "_TUTORIAL001.PY_COMPLETE": "install-source_pwsh", + "_TYPER_COMPLETE_TESTING": "True", + }, + ) + assert ( + "Register-ArgumentCompleter -Native -CommandName tutorial001.py -ScriptBlock $scriptblock" + in result.stdout + ) + + +def test_completion_install_source_noshell(): + result = subprocess.run( + ["coverage", "run", mod.__file__], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + env={ + **os.environ, + "_TUTORIAL001.PY_COMPLETE": "install-source_noshell", + "_TYPER_COMPLETE_TESTING": "True", + }, + ) + assert "" in result.stdout diff --git a/tests/test_completion/test_completion_show.py b/tests/test_completion/test_completion_show.py new file mode 100644 index 0000000..22fba4d --- /dev/null +++ b/tests/test_completion/test_completion_show.py @@ -0,0 +1,103 @@ +import os +import subprocess + +from first_steps import tutorial001 as mod + + +def test_completion_show_no_shell(): + result = subprocess.run( + ["coverage", "run", mod.__file__, "--show-completion"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + env={ + **os.environ, + "_TYPER_COMPLETE_TESTING": "True", + "_TYPER_COMPLETE_TEST_DISABLE_SHELL_DETECTION": "True", + }, + ) + assert "Error: --show-completion option requires an argument" in result.stderr + + +def test_completion_show_bash(): + result = subprocess.run( + ["coverage", "run", mod.__file__, "--show-completion", "bash"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + env={ + **os.environ, + "_TYPER_COMPLETE_TESTING": "True", + "_TYPER_COMPLETE_TEST_DISABLE_SHELL_DETECTION": "True", + }, + ) + assert ( + "complete -o default -F _tutorial001py_completion tutorial001.py" + in result.stdout + ) + + +def test_completion_source_zsh(): + result = subprocess.run( + ["coverage", "run", mod.__file__, "--show-completion", "zsh"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + env={ + **os.environ, + "_TYPER_COMPLETE_TESTING": "True", + "_TYPER_COMPLETE_TEST_DISABLE_SHELL_DETECTION": "True", + }, + ) + assert "compdef _tutorial001py_completion tutorial001.py" in result.stdout + + +def test_completion_source_fish(): + result = subprocess.run( + ["coverage", "run", mod.__file__, "--show-completion", "fish"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + env={ + **os.environ, + "_TYPER_COMPLETE_TESTING": "True", + "_TYPER_COMPLETE_TEST_DISABLE_SHELL_DETECTION": "True", + }, + ) + assert "complete --command tutorial001.py --no-files" in result.stdout + + +def test_completion_source_powershell(): + result = subprocess.run( + ["coverage", "run", mod.__file__, "--show-completion", "powershell"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + env={ + **os.environ, + "_TYPER_COMPLETE_TESTING": "True", + "_TYPER_COMPLETE_TEST_DISABLE_SHELL_DETECTION": "True", + }, + ) + assert ( + "Register-ArgumentCompleter -Native -CommandName tutorial001.py -ScriptBlock $scriptblock" + in result.stdout + ) + + +def test_completion_source_pwsh(): + result = subprocess.run( + ["coverage", "run", mod.__file__, "--show-completion", "pwsh"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + env={ + **os.environ, + "_TYPER_COMPLETE_TESTING": "True", + "_TYPER_COMPLETE_TEST_DISABLE_SHELL_DETECTION": "True", + }, + ) + assert ( + "Register-ArgumentCompleter -Native -CommandName tutorial001.py -ScriptBlock $scriptblock" + in result.stdout + ) diff --git a/tests/test_others.py b/tests/test_others.py index 1be59b9..e7741a5 100644 --- a/tests/test_others.py +++ b/tests/test_others.py @@ -1,6 +1,9 @@ from typing import Optional +from unittest import mock +import shellingham import typer +import typer.completion from typer.main import solve_typer_info_defaults, solve_typer_info_help from typer.models import TyperInfo from typer.testing import CliRunner @@ -49,3 +52,20 @@ def test_defaults_from_info(): # Mainly for coverage/completeness value = solve_typer_info_defaults(TyperInfo()) assert value + + +def test_install_invalid_shell(): + app = typer.Typer() + + @app.command() + def main(): + typer.echo("Hello World") + + typer.completion.Shells + with mock.patch.object( + shellingham, "detect_shell", return_value=("xshell", "/usr/bin/xshell") + ): + result = runner.invoke(app, ["--install-completion"]) + assert "Shell xshell is not supported." in result.stdout + result = runner.invoke(app) + assert "Hello World" in result.stdout diff --git a/typer/completion.py b/typer/completion.py index 4ef063b..02fb7a9 100644 --- a/typer/completion.py +++ b/typer/completion.py @@ -1,19 +1,44 @@ +import inspect +import os +import re +import subprocess import sys -from typing import Any +from enum import Enum +from pathlib import Path +from typing import Any, Optional, Tuple import click -import click_completion -import click_completion.core +import click._bashcomplete from .params import Option -click_completion.init() +try: + import shellingham +except ImportError: # pragma: nocover + shellingham = None + + +_click_patched = False + + +def get_completion_inspect_parameters() -> Tuple[inspect.Parameter, inspect.Parameter]: + completion_init() + test_disable_detection = os.getenv("_TYPER_COMPLETE_TEST_DISABLE_SHELL_DETECTION") + if shellingham and not test_disable_detection: + signature = inspect.signature(_install_completion_placeholder_function) + else: + signature = inspect.signature(_install_completion_no_auto_placeholder_function) + install_param, show_param = signature.parameters.values() + return install_param, show_param def install_callback(ctx: click.Context, param: click.Parameter, value: Any) -> Any: if not value or ctx.resilient_parsing: return value # pragma no cover - shell, path = click_completion.core.install() + if isinstance(value, str): + shell, path = install(shell=value) + else: + shell, path = install() click.secho(f"{shell} completion installed in {path}.", fg="green") click.echo("Completion will take effect once you restart the terminal.") sys.exit(0) @@ -22,10 +47,26 @@ def install_callback(ctx: click.Context, param: click.Parameter, value: Any) -> def show_callback(ctx: click.Context, param: click.Parameter, value: Any) -> Any: if not value or ctx.resilient_parsing: return value # pragma no cover - click.echo(click_completion.core.get_code()) + prog_name = ctx.find_root().info_name + assert prog_name + complete_var = "_{}_COMPLETE".format(prog_name.replace("-", "_").upper()) + if isinstance(value, str): + shell = value + elif shellingham: + shell, _ = shellingham.detect_shell() + script_content = get_completion_script(prog_name, complete_var, shell) + click.echo(script_content) sys.exit(0) +class Shells(str, Enum): + bash = "bash" + zsh = "zsh" + fish = "fish" + powershell = "powershell" + pwsh = "pwsh" + + # Create a fake command function to extract the completion parameters def _install_completion_placeholder_function( install_completion: bool = Option( @@ -46,3 +87,311 @@ def _install_completion_placeholder_function( ), ) -> Any: pass # pragma no cover + + +def _install_completion_no_auto_placeholder_function( + install_completion: Shells = Option( + None, + callback=install_callback, + expose_value=False, + help="Install completion for the specified shell.", + ), + show_completion: Shells = Option( + None, + callback=show_callback, + expose_value=False, + help="Show completion for the specified shell, to copy it or customize the installation.", + ), +) -> Any: + pass # pragma no cover + + +COMPLETION_SCRIPT_BASH = """ +%(complete_func)s() { + local IFS=$'\n' + COMPREPLY=( $( env COMP_WORDS="${COMP_WORDS[*]}" \\ + COMP_CWORD=$COMP_CWORD \\ + %(autocomplete_var)s=complete_bash $1 ) ) + return 0 +} + +complete -o default -F %(complete_func)s %(prog_name)s +""" + +COMPLETION_SCRIPT_ZSH = """ +#compdef %(prog_name)s + +%(complete_func)s() { + eval $(env _TYPER_COMPLETE_ARGS="${words[1,$CURRENT]}" %(autocomplete_var)s=complete_zsh %(prog_name)s) +} + +compdef %(complete_func)s %(prog_name)s +""" + +COMPLETION_SCRIPT_FISH = 'complete --command %(prog_name)s --no-files --arguments "(env %(autocomplete_var)s=complete_fish _TYPER_COMPLETE_FISH_ACTION=get-args _TYPER_COMPLETE_ARGS=(commandline -cp) %(prog_name)s)" --condition "env %(autocomplete_var)s=complete_fish _TYPER_COMPLETE_FISH_ACTION=is-args _TYPER_COMPLETE_ARGS=(commandline -cp) %(prog_name)s' + +COMPLETION_SCRIPT_POWER_SHELL = """ +Import-Module PSReadLine +Set-PSReadLineKeyHandler -Chord Tab -Function MenuComplete +$scriptblock = { + param($wordToComplete, $commandAst, $cursorPosition) + $Env:%(autocomplete_var)s = "complete_powershell" + $Env:_TYPER_COMPLETE_ARGS = $commandAst.ToString() + $Env:_TYPER_COMPLETE_WORD_TO_COMPLETE = $wordToComplete + %(prog_name)s | ForEach-Object { + $commandArray = $_ -Split ":::" + $command = $commandArray[0] + $helpString = $commandArray[1] + [System.Management.Automation.CompletionResult]::new( + $command, $command, 'ParameterValue', $helpString) + } + $Env:%(autocomplete_var)s = "" + $Env:_TYPER_COMPLETE_ARGS = "" + $Env:_TYPER_COMPLETE_WORD_TO_COMPLETE = "" +} +Register-ArgumentCompleter -Native -CommandName %(prog_name)s -ScriptBlock $scriptblock +""" + + +def install( + shell: Optional[str] = None, + prog_name: Optional[str] = None, + complete_var: Optional[str] = None, +) -> Tuple[str, Path]: + prog_name = prog_name or click.get_current_context().find_root().info_name + assert prog_name + if complete_var is None: + complete_var = "_{}_COMPLETE".format(prog_name.replace("-", "_").upper()) + if shell is None and shellingham is not None: + shell, _ = shellingham.detect_shell() + mode = None + if shell == "bash": + path_obj = Path.home() / ".bash_completion" + mode = mode or "a" + elif shell == "zsh": + path_obj = Path.home() / ".zshrc" + mode = mode or "a" + elif shell == "fish": + path_obj = Path.home() / f".config/fish/completions/{prog_name}.fish" + mode = mode or "w" + elif shell in {"powershell", "pwsh"}: + subprocess.run( + [ + shell, + "-Command", + "Set-ExecutionPolicy", + "Unrestricted", + "-Scope", + "CurrentUser", + ] + ) + result = subprocess.run( + [shell, "-NoProfile", "-Command", "echo", "$profile"], + check=True, + stdout=subprocess.PIPE, + ) + if result.returncode != 0: # pragma: nocover + click.echo("Couldn't get PowerShell user profile", err=True) + raise click.exceptions.Exit(result.returncode) + path_str = "" + if isinstance(result.stdout, str): # pragma: nocover + path_str = result.stdout + if isinstance(result.stdout, bytes): + try: + # PowerShell would be predominant in Windows + path_str = result.stdout.decode("windows-1252") + except UnicodeDecodeError: # pragma: nocover + try: + path_str = result.stdout.decode("utf8") + except UnicodeDecodeError: + click.echo("Couldn't decode the path automatically", err=True) + raise click.exceptions.Exit(1) + path_obj = Path(path_str.strip()) + mode = mode or "a" + else: + click.echo(f"Shell {shell} is not supported.") + raise click.exceptions.Exit(1) + parent_dir: Path = path_obj.parent + parent_dir.mkdir(parents=True, exist_ok=True) + script_content = get_installable_script(prog_name, complete_var, shell) + with path_obj.open(mode=mode) as f: + f.write(f"{script_content}\n") + return shell, path_obj + + +def get_installable_script(prog_name: str, complete_var: str, shell: str) -> str: + if shell == "bash": + return f'eval "$({complete_var}=source_bash {prog_name})"' + elif shell == "zsh": + return f'eval "$({complete_var}=source_szh {prog_name})"' + elif shell == "fish": + return f"eval (env {complete_var}=source_fish {prog_name})" + elif shell in {"powershell", "pwsh"}: + return get_completion_script(prog_name, complete_var, shell) + return "" + + +def do_bash_complete(cli: click.Command, prog_name: str) -> bool: + cwords = click.parser.split_arg_string(os.getenv("COMP_WORDS", "")) + cword = int(os.getenv("COMP_CWORD", 0)) + args = cwords[1:cword] + try: + incomplete = cwords[cword] + except IndexError: + incomplete = "" + + for item in click._bashcomplete.get_choices(cli, prog_name, args, incomplete): + click.echo(item[0]) + return True + + +def do_zsh_complete(cli: click.Command, prog_name: str) -> bool: + completion_args = os.getenv("_TYPER_COMPLETE_ARGS", "") + cwords = click.parser.split_arg_string(completion_args) + args = cwords[1:] + if args and not completion_args.endswith(" "): + incomplete = args[-1] + args = args[:-1] + else: + incomplete = "" + + def escape(s: str) -> str: + return ( + s.replace('"', '""') + .replace("'", "''") + .replace("$", "\\$") + .replace("`", "\\`") + ) + + res = [] + for item, help in click._bashcomplete.get_choices(cli, prog_name, args, incomplete): + if help: + res.append(f'"{escape(item)}":"{escape(help)}"') + else: + res.append(f'"{escape(item)}"') + if res: + args_str = "\n".join(res) + click.echo(f"_arguments '*: :(({args_str}))'") + else: + click.echo("_files") + return True + + +def do_fish_complete(cli: click.Command, prog_name: str) -> bool: + completion_args = os.getenv("_TYPER_COMPLETE_ARGS", "") + complete_action = os.getenv("_TYPER_COMPLETE_FISH_ACTION", "") + cwords = click.parser.split_arg_string(completion_args) + args = cwords[1:] + if args and not completion_args.endswith(" "): + incomplete = args[-1] + args = args[:-1] + else: + incomplete = "" + show_args = [] + for item, help in click._bashcomplete.get_choices(cli, prog_name, args, incomplete): + if help: + formatted_help = re.sub(r"\s", " ", help) + show_args.append(f"{item}\t{formatted_help}") + else: + show_args.append(item) + if complete_action == "get-args": + if show_args: + for arg in show_args: + click.echo(arg) + elif complete_action == "is-args": + if show_args: + # Activate complete args (no files) + sys.exit(0) + else: + # Deactivate complete args (allow files) + sys.exit(1) + return True + + +def do_powershell_complete(cli: click.Command, prog_name: str) -> bool: + completion_args = os.getenv("_TYPER_COMPLETE_ARGS", "") + incomplete = os.getenv("_TYPER_COMPLETE_WORD_TO_COMPLETE", "") + cwords = click.parser.split_arg_string(completion_args) + args = cwords[1:] + for item, help in click._bashcomplete.get_choices(cli, prog_name, args, incomplete): + click.echo(f"{item}:::{help or ' '}") + + return True + + +def do_shell_complete(*, cli: click.Command, prog_name: str, shell: str) -> bool: + if shell == "bash": + return do_bash_complete(cli, prog_name) + elif shell == "zsh": + return do_zsh_complete(cli, prog_name) + elif shell == "fish": + return do_fish_complete(cli, prog_name) + elif shell in {"powershell", "pwsh"}: + return do_powershell_complete(cli, prog_name) + return False + + +_completion_scripts = { + "bash": COMPLETION_SCRIPT_BASH, + "zsh": COMPLETION_SCRIPT_ZSH, + "fish": COMPLETION_SCRIPT_FISH, + "powershell": COMPLETION_SCRIPT_POWER_SHELL, + "pwsh": COMPLETION_SCRIPT_POWER_SHELL, +} + + +def get_completion_script(prog_name: str, complete_var: str, shell: str) -> str: + cf_name = click._bashcomplete._invalid_ident_char_re.sub( + "", prog_name.replace("-", "_") + ) + script = _completion_scripts.get(shell) + if script is None: + click.echo(f"Shell {shell} not supported.", err=True) + sys.exit(1) + return ( + script + % dict( + complete_func="_{}_completion".format(cf_name), + prog_name=prog_name, + autocomplete_var=complete_var, + ) + ).strip() + + +def handle_shell_complete( + cli: click.Command, prog_name: str, complete_var: str, complete_instr: str +) -> bool: + if "_" not in complete_instr: + click.echo("Invalid completion instruction.", err=True) + sys.exit(1) + command, shell = complete_instr.split("_", 1) + if command == "source": + click.echo(get_completion_script(prog_name, complete_var, shell)) + return True + elif command == "install-source": + click.echo(get_installable_script(prog_name, complete_var, shell)) + return True + elif command == "complete": + return do_shell_complete(cli=cli, prog_name=prog_name, shell=shell) + return False + + +def completion_init() -> None: + global _click_patched + if not _click_patched: + testing = os.getenv("_TYPER_COMPLETE_TESTING") + + def testing_handle_shell_complete( + cli: click.Command, prog_name: str, complete_var: str, complete_instr: str + ) -> bool: + result = handle_shell_complete(cli, prog_name, complete_var, complete_instr) + if result: + # Avoid fast_exit(1) in Click so Coverage can finish + sys.exit(1) + return result + + if testing: + click._bashcomplete.bashcomplete = testing_handle_shell_complete + else: + click._bashcomplete.bashcomplete = handle_shell_complete + _click_patched = True diff --git a/typer/main.py b/typer/main.py index 5217633..936fc31 100644 --- a/typer/main.py +++ b/typer/main.py @@ -8,6 +8,7 @@ from uuid import UUID import click +from .completion import get_completion_inspect_parameters from .models import ( AnyType, ArgumentInfo, @@ -26,16 +27,9 @@ from .models import ( TyperInfo, ) -try: - import click_completion - from .completion import _install_completion_placeholder_function -except ImportError: # pragma: no cover - click_completion = None - def get_install_completion_arguments() -> Tuple[click.Parameter, click.Parameter]: - signature = inspect.signature(_install_completion_placeholder_function) - install_param, show_param = signature.parameters.values() + install_param, show_param = get_completion_inspect_parameters() click_install_param, _ = get_click_param(install_param) click_show_param, _ = get_click_param(show_param) return click_install_param, click_show_param @@ -223,8 +217,7 @@ def get_group(typer_instance: Typer) -> click.Command: def get_command(typer_instance: Typer) -> click.Command: - if typer_instance._add_completion and click_completion: - click_completion.init() + if typer_instance._add_completion: click_install_param, click_show_param = get_install_completion_arguments() if ( typer_instance.registered_callback @@ -234,14 +227,14 @@ def get_command(typer_instance: Typer) -> click.Command: ): # Create a Group click_command = get_group(typer_instance) - if typer_instance._add_completion and click_completion: + if typer_instance._add_completion: click_command.params.append(click_install_param) click_command.params.append(click_show_param) return click_command elif len(typer_instance.registered_commands) == 1: # Create a single Command click_command = get_command_from_info(typer_instance.registered_commands[0]) - if typer_instance._add_completion and click_completion: + if typer_instance._add_completion: click_command.params.append(click_install_param) click_command.params.append(click_show_param) return click_command