Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

✨ Make typer.run() not add completion scripts by default, it only makes sense in installed apps #488

Merged
merged 7 commits into from Nov 5, 2022
40 changes: 39 additions & 1 deletion docs/tutorial/commands/index.md
Expand Up @@ -76,7 +76,7 @@ When you use `typer.run()`, **Typer** is doing more or less the same as above, i

In our case, this decorator tells **Typer** that the function below is a "`command`".

Both ways, with `typer.run()` and creating the explicit application, achieve the same.
Both ways, with `typer.run()` and creating the explicit application, achieve almost the same.

!!! tip
If your use case is solved with just `typer.run()`, that's fine, you don't have to create the explicit `app` and use `@app.command()`, etc.
Expand Down Expand Up @@ -114,6 +114,44 @@ Options:

</div>

## CLI application completion

There's a little detail that is worth noting here.

To get shell/tab completion, it's necessary to build a package that you and your users can install and **call directly**.

So instead of running a Python script like:

<div class="termy">

```console
$ python main.py

✨ Some magic here ✨
```

</div>

...It would be called like:

<div class="termy">

```console
$ magic-app

✨ Some magic here ✨
```

</div>

Having a standalone program like that allows setting up shell/tab completion.

The first step to be able to create an installable package like that is to use an explicit `typer.Typer()` app.

Later you can learn all the process to create a standalone CLI application and [Build a Package](../package.md){.internal-link target=_blank}.

But for now, it's just good to know that you are on that path. 😎

## A CLI application with multiple commands

Coming back to the CLI applications with multiple commands/subcommands, **Typer** allows creating CLI applications with multiple of them.
Expand Down
Expand Up @@ -12,10 +12,10 @@ After installing completion (for your own Python package or for **Typer CLI**),

To check it quickly without creating a new Python package, install [Typer CLI](../../typer-cli.md){.internal-link target=_blank}.

Then let's create small example script:
Then let's create small example program:

```Python
{!../docs_src/options/autocompletion/tutorial001.py!}
{!../docs_src/options_autocompletion/tutorial001.py!}
```

And let's try it with **Typer CLI** to get completion:
Expand Down Expand Up @@ -50,8 +50,8 @@ Right now we get completion for the *CLI option* names, but not for the values.

We can provide completion for the values creating an `autocompletion` function, similar to the `callback` functions from [CLI Option Callback and Context](./callback-and-context.md){.internal-link target=_blank}:

```Python hl_lines="4 5 10"
{!../docs_src/options/autocompletion/tutorial002.py!}
```Python hl_lines="4-5 14"
{!../docs_src/options_autocompletion/tutorial002.py!}
```

We return a `list` of strings from the `complete_name()` function.
Expand Down Expand Up @@ -81,8 +81,8 @@ Modify the `complete_name()` function to receive a parameter of type `str`, it w

Then we can check and return only the values that start with the incomplete value from the command line:

```Python hl_lines="6 7 8 9 10 11"
{!../docs_src/options/autocompletion/tutorial003.py!}
```Python hl_lines="6-11"
{!../docs_src/options_autocompletion/tutorial003.py!}
```

Now let's try it:
Expand Down Expand Up @@ -120,7 +120,7 @@ In the `complete_name()` function, instead of providing one `str` per completion
So, in the end, we return a `list` of `tuples` of `str`:

```Python hl_lines="3 4 5 6 7 10 11 12 13 14 15 16"
{!../docs_src/options/autocompletion/tutorial004.py!}
{!../docs_src/options_autocompletion/tutorial004.py!}
```

!!! tip
Expand Down Expand Up @@ -157,7 +157,7 @@ Instead of creating and returning a list with values (`str` or `tuple`), we can
That way our function will be a <a href="https://docs.python.org/3.8/glossary.html#index-19" class="external-link" target="_blank">generator</a> that **Typer** (actually Click) can iterate:

```Python hl_lines="10 11 12 13"
{!../docs_src/options/autocompletion/tutorial005.py!}
{!../docs_src/options_autocompletion/tutorial005.py!}
```

That simplifies our code a bit and works the same.
Expand Down Expand Up @@ -185,8 +185,8 @@ So, we will allow multiple `--name` *CLI options*.

For this we use a `List` of `str`:

```Python hl_lines="6 7 8"
{!../docs_src/options/autocompletion/tutorial006.py!}
```Python hl_lines="8-11"
{!../docs_src/options_autocompletion/tutorial006.py!}
```

And then we can use it like:
Expand All @@ -213,7 +213,7 @@ But you can access the context by declaring a function parameter of type `typer.
And from that context you can get the current values for each parameter.

```Python hl_lines="12 13 15"
{!../docs_src/options/autocompletion/tutorial007.py!}
{!../docs_src/options_autocompletion/tutorial007.py!}
```

We are getting the `names` already provided with `--name` in the command line before this completion was triggered.
Expand Down Expand Up @@ -282,7 +282,7 @@ You can print to "standard error" with a **Rich** `Console(stderr=True)`.
Using `stderr=True` tells **Rich** that the output should be shown in "standard error".

```Python hl_lines="12 15-16"
{!../docs_src/options/autocompletion/tutorial008.py!}
{!../docs_src/options_autocompletion/tutorial008.py!}
```

!!! info
Expand Down Expand Up @@ -323,7 +323,7 @@ Sebastian -- The type hints guy.
Of course, you can declare everything if you need it, the context, the raw *CLI parameters*, and the incomplete `str`:

```Python hl_lines="15"
{!../docs_src/options/autocompletion/tutorial009.py!}
{!../docs_src/options_autocompletion/tutorial009.py!}
```

Check it:
Expand Down
@@ -1,9 +1,12 @@
import typer

app = typer.Typer()


@app.command()
def main(name: str = typer.Option("World", help="The name to say hi to.")):
print(f"Hello {name}")


if __name__ == "__main__":
typer.run(main)
app()
Expand Up @@ -5,6 +5,10 @@ def complete_name():
return ["Camila", "Carlos", "Sebastian"]


app = typer.Typer()


@app.command()
def main(
name: str = typer.Option(
"World", help="The name to say hi to.", autocompletion=complete_name
Expand All @@ -14,4 +18,4 @@ def main(


if __name__ == "__main__":
typer.run(main)
app()
Expand Up @@ -11,6 +11,10 @@ def complete_name(incomplete: str):
return completion


app = typer.Typer()


@app.command()
def main(
name: str = typer.Option(
"World", help="The name to say hi to.", autocompletion=complete_name
Expand All @@ -20,4 +24,4 @@ def main(


if __name__ == "__main__":
typer.run(main)
app()
Expand Up @@ -16,6 +16,10 @@ def complete_name(incomplete: str):
return completion


app = typer.Typer()


@app.command()
def main(
name: str = typer.Option(
"World", help="The name to say hi to.", autocompletion=complete_name
Expand All @@ -25,4 +29,4 @@ def main(


if __name__ == "__main__":
typer.run(main)
app()
Expand Up @@ -13,6 +13,10 @@ def complete_name(incomplete: str):
yield (name, help_text)


app = typer.Typer()


@app.command()
def main(
name: str = typer.Option(
"World", help="The name to say hi to.", autocompletion=complete_name
Expand All @@ -22,4 +26,4 @@ def main(


if __name__ == "__main__":
typer.run(main)
app()
Expand Up @@ -2,11 +2,14 @@

import typer

app = typer.Typer()


@app.command()
def main(name: List[str] = typer.Option(["World"], help="The name to say hi to.")):
for each_name in name:
print(f"Hello {each_name}")


if __name__ == "__main__":
typer.run(main)
app()
Expand Up @@ -16,6 +16,10 @@ def complete_name(ctx: typer.Context, incomplete: str):
yield (name, help_text)


app = typer.Typer()


@app.command()
def main(
name: List[str] = typer.Option(
["World"], help="The name to say hi to.", autocompletion=complete_name
Expand All @@ -26,4 +30,4 @@ def main(


if __name__ == "__main__":
typer.run(main)
app()
Expand Up @@ -19,6 +19,10 @@ def complete_name(args: List[str], incomplete: str):
yield (name, help_text)


app = typer.Typer()


@app.command()
def main(
name: List[str] = typer.Option(
["World"], help="The name to say hi to.", autocompletion=complete_name
Expand All @@ -29,4 +33,4 @@ def main(


if __name__ == "__main__":
typer.run(main)
app()
Expand Up @@ -20,6 +20,10 @@ def complete_name(ctx: typer.Context, args: List[str], incomplete: str):
yield (name, help_text)


app = typer.Typer()


@app.command()
def main(
name: List[str] = typer.Option(
["World"], help="The name to say hi to.", autocompletion=complete_name
Expand All @@ -30,4 +34,4 @@ def main(


if __name__ == "__main__":
typer.run(main)
app()
2 changes: 1 addition & 1 deletion mkdocs.yml
Expand Up @@ -41,7 +41,6 @@ nav:
- CLI Option Name: tutorial/options/name.md
- CLI Option Callback and Context: tutorial/options/callback-and-context.md
- Version CLI Option, is_eager: tutorial/options/version.md
- CLI Option autocompletion: tutorial/options/autocompletion.md
- Commands:
- Commands Intro: tutorial/commands/index.md
- Command CLI Arguments: tutorial/commands/arguments.md
Expand All @@ -51,6 +50,7 @@ nav:
- Typer Callback: tutorial/commands/callback.md
- One or Multiple Commands: tutorial/commands/one-or-multiple.md
- Using the Context: tutorial/commands/context.md
- CLI Option autocompletion: tutorial/options-autocompletion.md
- CLI Parameter Types:
- CLI Parameter Types Intro: tutorial/parameter-types/index.md
- Number: tutorial/parameter-types/number.md
Expand Down
16 changes: 16 additions & 0 deletions pyproject.toml
Expand Up @@ -77,3 +77,19 @@ skip_glob = [
"docs_src/subcommands/tutorial003/lands.py",
"docs_src/subcommands/tutorial003/main.py",
]

[tool.pytest.ini_options]
addopts = [
"--strict-config",
"--strict-markers",
]
xfail_strict = true
junit_family = "xunit2"
filterwarnings = [
"error",
# TODO: until I refactor completion to use the new shell_complete
"ignore:'autocompletion' is renamed to 'shell_complete'. The old name is deprecated and will be removed in Click 8.1. See the docs about 'Parameter' for information about new behavior.:DeprecationWarning:typer",
'ignore:starlette.middleware.wsgi is deprecated and will be removed in a future release\..*:DeprecationWarning:starlette',
# For pytest-xdist
'ignore::DeprecationWarning:xdist',
]
2 changes: 1 addition & 1 deletion tests/test_completion/test_completion.py
Expand Up @@ -3,7 +3,7 @@
import sys
from pathlib import Path

from docs_src.first_steps import tutorial001 as mod
from docs_src.commands.index import tutorial001 as mod


def test_show_completion():
Expand Down
2 changes: 1 addition & 1 deletion tests/test_completion/test_completion_install.py
Expand Up @@ -7,7 +7,7 @@
import typer
from typer.testing import CliRunner

from docs_src.first_steps import tutorial001 as mod
from docs_src.commands.index import tutorial001 as mod

runner = CliRunner()
app = typer.Typer()
Expand Down
2 changes: 1 addition & 1 deletion tests/test_completion/test_completion_show.py
@@ -1,7 +1,7 @@
import os
import subprocess

from docs_src.first_steps import tutorial001 as mod
from docs_src.commands.index import tutorial001 as mod


def test_completion_show_no_shell():
Expand Down
@@ -1,16 +1,12 @@
import os
import subprocess

import typer
from typer.testing import CliRunner

from docs_src.options.autocompletion import tutorial002 as mod
from docs_src.options_autocompletion import tutorial002 as mod

runner = CliRunner()

app = typer.Typer()
app.command()(mod.main)


def test_completion():
result = subprocess.run(
Expand All @@ -31,7 +27,7 @@ def test_completion():


def test_1():
result = runner.invoke(app, ["--name", "Camila"])
result = runner.invoke(mod.app, ["--name", "Camila"])
assert result.exit_code == 0
assert "Hello Camila" in result.output

Expand Down