✨ Re-implement completion, fixes, add PowerShell support (#66)
* ✨ Re-implement completion * ✅ Add tests for re-implemented completion * 🎨 Move mypy config to file * 🙈 Update .gitignore * ➕ Remove click-completion, add support for autodetection with shellingham * ✅ Fix test for PowerShell * 🐛 Fix PowerShell permissions/test * 🎨 Format test * 🏁 Fix PowerShell script for Windows and PowerShell 5 * 📝 Update docs, internal implementation of completion
This commit is contained in:
parent
d651b7dc77
commit
1d3337a4da
19 changed files with 1198 additions and 86 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -9,3 +9,4 @@ dist
|
|||
site
|
||||
.coverage
|
||||
htmlcov
|
||||
.pytest_cache
|
||||
|
|
10
README.md
10
README.md
|
@ -31,7 +31,7 @@ Typer is library to build <abbr title="command line interface, programs executed
|
|||
The key features are:
|
||||
|
||||
* **Intuitive to write**: Great editor support. <abbr title="also known as auto-complete, autocompletion, IntelliSense">Completion</abbr> 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 <a href="https://typer.tiangolo.com/typer-cli/" class="internal-link" target="_blank">Typer CLI</a>.
|
||||
**Your users get**: automatic **`--help`**, **auto completion** in their terminal (Bash, Zsh, Fish, PowerShell) when they install your package or when using <a href="https://typer.tiangolo.com/typer-cli/" class="internal-link" target="_blank">Typer CLI</a>.
|
||||
|
||||
For a more complete example including more features, see the <a href="https://typer.tiangolo.com/tutorial/">Tutorial - User Guide</a>.
|
||||
|
||||
|
@ -256,9 +256,11 @@ But you can also install extras:
|
|||
* <a href="https://pypi.org/project/colorama/" class="external-link" target="_blank"><code>colorama</code></a>: 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. <a href="https://pypi.org/project/wasabi/" class="external-link" target="_blank"><code>wasabi</code></a>, <a href="https://github.com/erikrose/blessings" class="external-link" target="_blank"><code>blessings</code></a>.
|
||||
* <a href="https://github.com/click-contrib/click-completion" class="external-link" target="_blank"><code>click-completion</code></a>: and Typer will automatically configure it to provide completion for all the shells, including installation commands.
|
||||
* <a href="https://github.com/sarugaku/shellingham" class="external-link" target="_blank"><code>shellingham</code></a>: 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
|
||||
|
||||
|
|
|
@ -63,6 +63,17 @@ It was built with some great ideas and design using the features available in th
|
|||
|
||||
As someone pointed out: <em><a href="https://twitter.com/fishnets88/status/1210126833745838080" class="external-link" target="_blank">"Nice to see it is built on Click but adds the type stuff. Me gusta!"</a></em>
|
||||
|
||||
### <a href="https://github.com/click-contrib/click-completion" class="external-link" target="_blank">`click-completion`</a>
|
||||
|
||||
`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.
|
||||
|
||||
### <a href="https://fastapi.tiangolo.com/" class="external-link" target="_blank">FastAPI</a>
|
||||
|
||||
I created **FastAPI** to provide an easy way to build APIs with autocompletion for everything in the code (and some other <a href="https://fastapi.tiangolo.com/features/" class="external-link" target="_blank">features</a>).
|
||||
|
|
|
@ -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 <kbd>TAB</kbd> 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
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ Typer is library to build <abbr title="command line interface, programs executed
|
|||
The key features are:
|
||||
|
||||
* **Intuitive to write**: Great editor support. <abbr title="also known as auto-complete, autocompletion, IntelliSense">Completion</abbr> 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 <a href="https://typer.tiangolo.com/typer-cli/" class="internal-link" target="_blank">Typer CLI</a>.
|
||||
**Your users get**: automatic **`--help`**, **auto completion** in their terminal (Bash, Zsh, Fish, PowerShell) when they install your package or when using <a href="https://typer.tiangolo.com/typer-cli/" class="internal-link" target="_blank">Typer CLI</a>.
|
||||
|
||||
For a more complete example including more features, see the <a href="https://typer.tiangolo.com/tutorial/">Tutorial - User Guide</a>.
|
||||
|
||||
|
@ -256,9 +256,11 @@ But you can also install extras:
|
|||
* <a href="https://pypi.org/project/colorama/" class="external-link" target="_blank"><code>colorama</code></a>: 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. <a href="https://pypi.org/project/wasabi/" class="external-link" target="_blank"><code>wasabi</code></a>, <a href="https://github.com/erikrose/blessings" class="external-link" target="_blank"><code>blessings</code></a>.
|
||||
* <a href="https://github.com/click-contrib/click-completion" class="external-link" target="_blank"><code>click-completion</code></a>: and Typer will automatically configure it to provide completion for all the shells, including installation commands.
|
||||
* <a href="https://github.com/sarugaku/shellingham" class="external-link" target="_blank"><code>shellingham</code></a>: 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
|
||||
|
||||
|
|
4
mypy.ini
4
mypy.ini
|
@ -1,2 +1,6 @@
|
|||
[mypy]
|
||||
ignore_missing_imports = True
|
||||
disallow_untyped_defs = True
|
||||
|
||||
[mypy-tests.*]
|
||||
disallow_untyped_defs = False
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
0
tests/test_completion/__init__.py
Normal file
0
tests/test_completion/__init__.py
Normal file
174
tests/test_completion/test_completion.py
Normal file
174
tests/test_completion/test_completion.py
Normal file
|
@ -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
|
||||
)
|
178
tests/test_completion/test_completion_complete.py
Normal file
178
tests/test_completion/test_completion_complete.py
Normal file
|
@ -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
|
69
tests/test_completion/test_completion_complete_no_help.py
Normal file
69
tests/test_completion/test_completion_complete_no_help.py
Normal file
|
@ -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
|
140
tests/test_completion/test_completion_install.py
Normal file
140
tests/test_completion/test_completion_install.py
Normal file
|
@ -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
|
107
tests/test_completion/test_completion_install_source.py
Normal file
107
tests/test_completion/test_completion_install_source.py
Normal file
|
@ -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
|
103
tests/test_completion/test_completion_show.py
Normal file
103
tests/test_completion/test_completion_show.py
Normal file
|
@ -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
|
||||
)
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue