diff --git a/README.md b/README.md index a15ac0090f..e8ec1a3135 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ Options: --show-completion Show completion for the current shell, to copy it or customize the installation. --help Show this message and exit. -// You get a ✨ auto completion ✨ for free, installed with --install-completion +// You get ✨ auto completion ✨ for free, installed with --install-completion // Now pass the NAME argument $ python main.py Camila diff --git a/docs/css/custom.css b/docs/css/custom.css index 4c9a17852e..d4063f29d6 100644 --- a/docs/css/custom.css +++ b/docs/css/custom.css @@ -1,4 +1,9 @@ .termynal-comment { - color: #999; + color: #4a968f; font-style: italic; + display: block; +} + +.termy { + white-space: pre-wrap; } diff --git a/docs/index.md b/docs/index.md index a15ac0090f..e8ec1a3135 100644 --- a/docs/index.md +++ b/docs/index.md @@ -98,7 +98,7 @@ Options: --show-completion Show completion for the current shell, to copy it or customize the installation. --help Show this message and exit. -// You get a ✨ auto completion ✨ for free, installed with --install-completion +// You get ✨ auto completion ✨ for free, installed with --install-completion // Now pass the NAME argument $ python main.py Camila diff --git a/docs/js/custom.js b/docs/js/custom.js index bbd0b29317..cd21d165e7 100644 --- a/docs/js/custom.js +++ b/docs/js/custom.js @@ -6,6 +6,7 @@ document.querySelectorAll(".use-termynal").forEach(node => { }); const progressLiteralStart = "---> 100%"; const promptLiteralStart = "$ "; +const customPromptLiteralStart = "# "; const termynalActivateClass = "termy"; let termynals = []; @@ -29,8 +30,8 @@ function createTermynals() { if (isBlankSpace) { dataValue["delay"] = 0; } - if (buffer.length > 1 && buffer[buffer.length - 1] === "") { - // The last single
won't have effect + if (buffer[buffer.length - 1] === "") { + // A last single
won't have effect // so put an additional one buffer.push(""); } @@ -55,12 +56,25 @@ function createTermynals() { }); } else if (line.startsWith("// ")) { saveBuffer(); - const value = line.replace("// ", "").trimEnd(); + const value = "💬 " + line.replace("// ", "").trimEnd(); useLines.push({ value: value, class: "termynal-comment", delay: 0 }); + } else if (line.startsWith(customPromptLiteralStart)) { + saveBuffer(); + const promptStart = line.indexOf(promptLiteralStart); + if (promptStart === -1) { + console.error("Custom prompt found but no end delimiter", line) + } + const prompt = line.slice(0, promptStart).replace(customPromptLiteralStart, "") + let value = line.slice(promptStart + promptLiteralStart.length); + useLines.push({ + type: "input", + value: value, + prompt: prompt + }); } else { buffer.push(line); } diff --git a/docs/js/termynal.js b/docs/js/termynal.js index e66321effe..47f7f47e62 100644 --- a/docs/js/termynal.js +++ b/docs/js/termynal.js @@ -243,12 +243,10 @@ class Termynal { attrs += ` class=${line[prop]} ` continue } - attrs += this.pfx; - if (prop === 'type') { - attrs += `="${line[prop]}" ` + attrs += `${this.pfx}="${line[prop]}" ` } else if (prop !== 'value') { - attrs += `-${prop}="${line[prop]}" ` + attrs += `${this.pfx}-${prop}="${line[prop]}" ` } } diff --git a/docs/release-notes.md b/docs/release-notes.md index 5b620f0b57..bb7e52a70d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -3,7 +3,7 @@ ## 0.0.5 * Clean exports from Typer. Remove unneeded components from Click and add needed `Exit` exception. PR [#11](https://github.com/tiangolo/typer/pull/11). -* Fix and document extracting help from a functions' docstring [First Steps: Document your CLI app](https://typer.tiangolo.com/tutorial/first-steps/#document-your-cli-app). PR [#10](https://github.com/tiangolo/typer/pull/10). +* Fix and document extracting help from a function's docstring [First Steps: Document your CLI app](https://typer.tiangolo.com/tutorial/first-steps/#document-your-cli-app). PR [#10](https://github.com/tiangolo/typer/pull/10). * Update references to `--install-completion` and `--show-completion` in docs. PR [#9](https://github.com/tiangolo/typer/pull/9). * Fix testing utilities, add tests for First Steps examples. PR [#8](https://github.com/tiangolo/typer/pull/8). * Add auto completion options by default when [click-completion](https://github.com/click-contrib/click-completion) is installed: `--install-completion` and `--show-completion`. PR [#7](https://github.com/tiangolo/typer/pull/7). diff --git a/docs/src/options/tutorial001.py b/docs/src/options/tutorial001.py new file mode 100644 index 0000000000..1e3bddcd30 --- /dev/null +++ b/docs/src/options/tutorial001.py @@ -0,0 +1,21 @@ +import typer + + +def main( + name: str, + lastname: str = typer.Option("", help="Last name of person to greet."), + formal: bool = typer.Option(False, help="Say hi formally."), +): + """ + Say hi to NAME, optionally with a --lastname. + + If --formal is used, say hi very formally. + """ + if formal: + typer.echo(f"Good day Ms. {name} {lastname}.") + else: + typer.echo(f"Hello {name} {lastname}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs/src/options/tutorial002.py b/docs/src/options/tutorial002.py new file mode 100644 index 0000000000..f62221b75b --- /dev/null +++ b/docs/src/options/tutorial002.py @@ -0,0 +1,9 @@ +import typer + + +def main(name: str, lastname: str = typer.Option(...)): + typer.echo(f"Hello {name} {lastname}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs/src/options/tutorial003.py b/docs/src/options/tutorial003.py new file mode 100644 index 0000000000..2da2592455 --- /dev/null +++ b/docs/src/options/tutorial003.py @@ -0,0 +1,9 @@ +import typer + + +def main(name: str, lastname: str = typer.Option(..., prompt=True)): + typer.echo(f"Hello {name} {lastname}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs/src/options/tutorial004.py b/docs/src/options/tutorial004.py new file mode 100644 index 0000000000..d9a19a9c76 --- /dev/null +++ b/docs/src/options/tutorial004.py @@ -0,0 +1,12 @@ +import typer + + +def main( + name: str, + lastname: str = typer.Option(..., prompt="Please tell me your last name"), +): + typer.echo(f"Hello {name} {lastname}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs/src/options/tutorial005.py b/docs/src/options/tutorial005.py new file mode 100644 index 0000000000..11471d7a64 --- /dev/null +++ b/docs/src/options/tutorial005.py @@ -0,0 +1,9 @@ +import typer + + +def main(project_name: str = typer.Option(..., prompt=True, confirmation_prompt=True)): + typer.echo(f"Deleting project {project_name}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs/src/options/tutorial006.py b/docs/src/options/tutorial006.py new file mode 100644 index 0000000000..9beb205b01 --- /dev/null +++ b/docs/src/options/tutorial006.py @@ -0,0 +1,9 @@ +import typer + + +def main(fullname: str = typer.Option("Wade Wilson", show_default=True)): + typer.echo(f"Hello {fullname}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs/tutorial/first-steps.md b/docs/tutorial/first-steps.md index 92316261c0..fedad69580 100644 --- a/docs/tutorial/first-steps.md +++ b/docs/tutorial/first-steps.md @@ -22,7 +22,7 @@ Hello World // Now check the --help $ python main.py --help -Usage: tryit.py [OPTIONS] +Usage: main.py [OPTIONS] Options: --install-completion Install completion for the current shell. @@ -174,16 +174,28 @@ $ ls --size ./myproject The main visual difference between a *CLI option* and and a *CLI argument* is that the *CLI option* has `--` prepended to the name, like in "`--size`". -A *CLI option* doesn't depend on the order because it has a predefined name (here it's `--size`). This is because the CLI app is looking specifically for a literal `--size` parameter or "flag", with that specific "name" (here the specific name is "`--size`"). The CLI app will check if you typed it or not, it will be actively looking for `--size` even if you didn't type it (to check if it's there or not). +A *CLI option* doesn't depend on the order because it has a predefined name (here it's `--size`). This is because the CLI app is looking specifically for a literal `--size` parameter (also known as "flag" or "switch"), with that specific "name" (here the specific name is "`--size`"). The CLI app will check if you typed it or not, it will be actively looking for `--size` even if you didn't type it (to check if it's there or not). -In contrast, the CLI app is not actively looking for the *CLI argument* with a text "`./myproject`", it has no way to know if you would type `./myproject` or `./mysuperawesomeproject` or anything else. It's just waiting to get whatever you give it. The only way to know that you refer to a specific *CLI argument* is because of the order. The same way that it knows that the first *CLI argument* was the `name` and the second was the `lastname`, but if you mixed the order, it wouldn't be able to handle it. +In contrast, the CLI app is not actively looking for the *CLI argument* with a text "`./myproject`", it has no way to know if you would type `./myproject` or `./my-super-awesome-project` or anything else. It's just waiting to get whatever you give it. The only way to know that you refer to a specific *CLI argument* is because of the order. The same way that it knows that the first *CLI argument* was the `name` and the second was the `lastname`, but if you mixed the order, it wouldn't be able to handle it. Instead, with a *CLI option*, the order doesn't matter. Also, by default, a *CLI option* is *optional* (not *required*). +So, by default: + +* A *CLI argument* is **required** +* A *CLI option* is **optional** + +But the *required* and *optional* defaults can be changed. + +So, the main and **most important** difference is that: + +* *CLI options* **start with `--`** and don't depend on the order +* *CLI arguments* depend on the **sequence order** + !!! tip - In this example above the *CLI option* `--size` is just a "flag", it's a boolean value, `True` or `False`. + In this example above the *CLI option* `--size` is just a "flag" or "switch" that will contain a boolean value, `True` or `False`, depending on if it was added to the command or not. This one doesn't receive any values. But *CLI options* can also receive values like *CLI arguments*. You'll see how later. @@ -319,7 +331,7 @@ Now see it with the `--help` option: ```console $ python main.py --help -Usage: tutorial006.py [OPTIONS] NAME +Usage: main.py [OPTIONS] NAME Say hi to NAME, optionally with a --lastname. @@ -394,11 +406,11 @@ And a parameter like `name`, that doesn't have a default value, is considered *r ### In CLIs -When talking about command line interfaces/applications, the words "argument" and "parameter" are commonly used to refer to that data passed to a CLI app, those parameters. +When talking about command line interfaces/applications, the words **"argument"** and **"parameter"** are commonly used to refer to that data passed to a CLI app, those parameters. -But those words don't imply anything about the data being required, needing to be passed in a certain order, nor having a flag like `--lastname`. +But those words **don't imply** anything about the data being required, needing to be passed in a certain order, nor having a flag like `--lastname`. -The parameters that come with a flag like `--lastname` and a value (or when the flag itself is the value, like `--formal`) are commonly optional, not required. So, when talking about CLIs it's common to call them *optional arguments* or *optional parameters*. +The parameters that come with a name like `--lastname` (and optionally a value) are commonly optional, not required. So, when talking about CLIs it's common to call them **optional arguments** or **optional parameters**. Sometimes these *optional parameters* that start with `--` are also called a **flag** or a **switch**. In reality, the parameters that require an order can be made *optional* too. And the ones that come with a flag (like `--lastname`) can be *required* too. @@ -408,6 +420,6 @@ To try and make it a bit easier, we'll normally use the words "parameter" or "ar We'll use ***CLI argument*** to refer to those *CLI parameters* that depend on an order. That are **required** by default. -And we'll use ***CLI option*** to refer to those *CLI parameters* that depend on a flag (like `--lastname`). That are **optional** by default. +And we'll use ***CLI option*** to refer to those *CLI parameters* that depend on a name that starts with `--` (like `--lastname`). That are **optional** by default. We will use ***CLI parameter*** to refer to both, *CLI arguments* and *CLI options*. diff --git a/docs/tutorial/options.md b/docs/tutorial/options.md new file mode 100644 index 0000000000..a1dc05ddd9 --- /dev/null +++ b/docs/tutorial/options.md @@ -0,0 +1,263 @@ +## *CLI options* with help + +In the *First Steps* section you saw how to add help for a CLI app/command by adding it to a function's docstring. + +Here's how that last example looked like: + +```Python +{!./src/first_steps/tutorial006.py!} +``` + +Now we'll add a *help* section to the *CLI options*: + +```Python hl_lines="6 7" +{!./src/options/tutorial001.py!} +``` + +We are replacing the default values we had before with `typer.Option()`. + +As we no longer have a default value there, the first parameter to `typer.Option()` serves the same purpose of defining that default value. + +So, if we had: + +```Python +lastname: str = "" +``` + +now we write: + +```Python +lastname: str = typer.Option("") +``` + +And both forms achieve the same: a *CLI option* with a default value of an empty string (`""`). + +And then we can pass the `help` keyword parameter: + +```Python +lastname: str = typer.Option("", help="this option does this and that") +``` + +to create the help for that *CLI option*. + +Copy that example from above to a file `main.py`. + +Test it: + +
+ +```console +$ python main.py --help + +Usage: main.py [OPTIONS] NAME + + Say hi to NAME, optionally with a --lastname. + + If --formal is used, say hi very formally. + +Options: + --lastname TEXT Last name of person to greet. + --formal / --no-formal Say hi formally. + --install-completion Install completion for the current shell. + --show-completion Show completion for the current shell, to copy it or customize the installation. + --help Show this message and exit. + +// Now you have a help text for the --lastname and --formal CLI options 🎉 +``` + +
+ +## Make a *CLI option* required + +We said before that *by default*: + +* *CLI options* are **optional** +* *CLI arguments* are **required** + +Well, that's how they work *by default*, and that's the convention in many CLI programs and systems. + +But if you really want, you can change that. + +To make a *CLI option* required, pass `...` to `typer.Option()`. + +!!! info + If you hadn't seen that `...` before: it is a a special single value, it is part of Python and is called "Ellipsis". + +That will tell **Typer** that it's still a *CLI option*, but it doesn't have a default value, and it's required. + +Let's make the `--lastname` a required *CLI option*. + +We'll also simplify the example to focus on the new parts: + +```Python hl_lines="4" +{!./src/options/tutorial002.py!} +``` + +!!! tip + You could still add `help` to `typer.Option()` as before, but we are omitting it here to simplify the example. + +And test it: + +
+ +```console +// Pass the NAME CLI argument +$ python main.py Camila + +// We didn't pass the now required --lastname CLI option +Usage: main.py [OPTIONS] NAME +Try "main.py --help" for help. + +Error: Missing option "--lastname". + +// Now update it to pass the required --lastname CLI option +$ python main.py Camila --lastname Gutiérrez + +Hello Camila Gutiérrez + +// And if you check the help +$ python main.py --help + +Usage: main.py [OPTIONS] NAME + +Options: + --lastname TEXT [required] + --install-completion Install completion for the current shell. + --show-completion Show completion for the current shell, to copy it or customize the installation. + --help Show this message and exit. + +// It now tells you that --lastname is required 🎉 +``` + +
+ +## Prompt for a *CLI option* + +It's also possible to, instead of just showing an error, ask for the missing value with `prompt=True`: + +```Python hl_lines="4" +{!./src/options/tutorial003.py!} +``` + +And then your program will ask the user for it in the terminal: + +
+ +```console +// Call it with the NAME CLI argument +$ python main.py Camila + +// It asks for the missing CLI option --lastname +# Lastname: $ Gutiérrez + +Hello Camila Gutiérrez +``` + +
+ +### Customize the prompt + +You can also set a custom prompt, passing the string that you want to use instead of just `True`: + +```Python hl_lines="6" +{!./src/options/tutorial004.py!} +``` + +And then your program will ask for it using with your custom prompt: + +
+ +```console +// Call it with the NAME CLI argument +$ python main.py Camila + +// It uses the custom prompt +# Please tell me your last name: $ Gutiérrez + +Hello Camila Gutiérrez +``` + +
+ +## Confirmation prompt + +In some cases you could want to prompt for something and then ask the user to confirm it by typing it twice. + +You can do it passing the parameter `confirmation_prompt=True`. + +Let's say it's a CLI app to delete a project: + +```Python hl_lines="4" +{!./src/options/tutorial005.py!} +``` + +And it will prompt the user for a value and then for the confirmation: + +
+ +```console +$ python main.py + +// Your app will first prompt for the project name, and then for the confirmation +# Project name: $ Old Project +# Repeat for confirmation: $ Old Project + +Deleting project Old Project + +// If the user doesn't type the same, receives an error and a new prompt +$ python main.py + +# Project name: $ Old Project +# Repeat for confirmation: $ New Spice + +Error: the two entered values do not match + +# Project name: $ Old Project +# Repeat for confirmation: $ Old Project + +Deleting project Old Project + +// Now it works 🎉 +``` + +
+ +## Show default in help + +You can tell Typer to show the default value in the help text with `show_default=True`: + +```Python hl_lines="4" +{!./src/options/tutorial006.py!} +``` + +And it will show up in the help text: + +
+ +```console +$ python main.py + +Hello Wade Wilson + +// Show the help +$ python main.py --help + +Usage: main.py [OPTIONS] + +Options: + --fullname TEXT [default: Wade Wilson] + --install-completion Install completion for the current shell. + --show-completion Show completion for the current shell, to copy it or customize the installation. + --help Show this message and exit. +``` + +
+ +!!! tip + Notice the `[default: Wade Wilson]` in the help text. + +## Other uses + +`typer.Option()` has several other users. For data validation, to enable other features, etc. + +But you will see about that later in the docs. diff --git a/mkdocs.yml b/mkdocs.yml index 1de9a0722a..be5ed5f798 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -23,6 +23,7 @@ nav: - Tutorial - User Guide: - Tutorial - User Guide - Intro: 'tutorial/index.md' - First Steps: 'tutorial/first-steps.md' + - CLI Options: 'tutorial/options.md' - Alternatives, Inspiration and Comparisons: 'alternatives.md' - Help Typer - Get Help: 'help-typer.md' - Development - Contributing: 'contributing.md' diff --git a/tests/test_tutorial/test_options/__init__.py b/tests/test_tutorial/test_options/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_tutorial/test_options/test_tutorial001.py b/tests/test_tutorial/test_options/test_tutorial001.py new file mode 100644 index 0000000000..835ed58358 --- /dev/null +++ b/tests/test_tutorial/test_options/test_tutorial001.py @@ -0,0 +1,48 @@ +import subprocess + +import typer +from typer.testing import CliRunner + +from options import tutorial001 as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_help(): + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "Say hi to NAME, optionally with a --lastname." in result.output + assert "If --formal is used, say hi very formally." in result.output + assert "Last name of person to greet." in result.output + assert "Say hi formally." in result.output + + +def test_1(): + result = runner.invoke(app, ["Camila"]) + assert result.exit_code == 0 + assert "Hello Camila" in result.output + + +def test_option_lastname(): + result = runner.invoke(app, ["Camila", "--lastname", "Gutiérrez"]) + assert result.exit_code == 0 + assert "Hello Camila Gutiérrez" in result.output + + +def test_formal(): + result = runner.invoke(app, ["Camila", "--lastname", "Gutiérrez", "--formal"]) + assert result.exit_code == 0 + assert "Good day Ms. Camila Gutiérrez." in result.output + + +def test_script(): + result = subprocess.run( + ["coverage", "run", mod.__file__, "--help"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_options/test_tutorial002.py b/tests/test_tutorial/test_options/test_tutorial002.py new file mode 100644 index 0000000000..77c880a11d --- /dev/null +++ b/tests/test_tutorial/test_options/test_tutorial002.py @@ -0,0 +1,40 @@ +import subprocess + +import typer +from typer.testing import CliRunner + +from options import tutorial002 as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_1(): + result = runner.invoke(app, ["Camila"]) + assert result.exit_code != 0 + assert 'Error: Missing option "--lastname".' in result.output + + +def test_option_lastname(): + result = runner.invoke(app, ["Camila", "--lastname", "Gutiérrez"]) + assert result.exit_code == 0 + assert "Hello Camila Gutiérrez" in result.output + + +def test_help(): + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "--lastname TEXT" in result.output + assert "[required]" in result.output + + +def test_script(): + result = subprocess.run( + ["coverage", "run", mod.__file__, "--help"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_options/test_tutorial003.py b/tests/test_tutorial/test_options/test_tutorial003.py new file mode 100644 index 0000000000..129e5d0160 --- /dev/null +++ b/tests/test_tutorial/test_options/test_tutorial003.py @@ -0,0 +1,41 @@ +import subprocess + +import typer +from typer.testing import CliRunner + +from options import tutorial003 as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_option_lastname(): + result = runner.invoke(app, ["Camila", "--lastname", "Gutiérrez"]) + assert result.exit_code == 0 + assert "Hello Camila Gutiérrez" in result.output + + +def test_option_lastname_prompt(): + result = runner.invoke(app, ["Camila"], input="Gutiérrez") + assert result.exit_code == 0 + assert "Lastname: " in result.output + assert "Hello Camila Gutiérrez" in result.output + + +def test_help(): + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "--lastname TEXT" in result.output + assert "[required]" in result.output + + +def test_script(): + result = subprocess.run( + ["coverage", "run", mod.__file__, "--help"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_options/test_tutorial004.py b/tests/test_tutorial/test_options/test_tutorial004.py new file mode 100644 index 0000000000..7946248815 --- /dev/null +++ b/tests/test_tutorial/test_options/test_tutorial004.py @@ -0,0 +1,41 @@ +import subprocess + +import typer +from typer.testing import CliRunner + +from options import tutorial004 as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_option_lastname(): + result = runner.invoke(app, ["Camila", "--lastname", "Gutiérrez"]) + assert result.exit_code == 0 + assert "Hello Camila Gutiérrez" in result.output + + +def test_option_lastname_prompt(): + result = runner.invoke(app, ["Camila"], input="Gutiérrez") + assert result.exit_code == 0 + assert "Please tell me your last name: " in result.output + assert "Hello Camila Gutiérrez" in result.output + + +def test_help(): + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "--lastname TEXT" in result.output + assert "[required]" in result.output + + +def test_script(): + result = subprocess.run( + ["coverage", "run", mod.__file__, "--help"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_options/test_tutorial005.py b/tests/test_tutorial/test_options/test_tutorial005.py new file mode 100644 index 0000000000..3b4557a9fc --- /dev/null +++ b/tests/test_tutorial/test_options/test_tutorial005.py @@ -0,0 +1,50 @@ +import subprocess + +import typer +from typer.testing import CliRunner + +from options import tutorial005 as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_prompt(): + result = runner.invoke(app, input="Old Project\nOld Project\n") + assert result.exit_code == 0 + assert "Deleting project Old Project" in result.output + + +def test_prompt_not_equal(): + result = runner.invoke( + app, input="Old Project\nNew Spice\nOld Project\nOld Project\n" + ) + assert result.exit_code == 0 + assert "Error: the two entered values do not match" in result.output + assert "Deleting project Old Project" in result.output + + +def test_option(): + result = runner.invoke(app, ["--project-name", "Old Project"]) + assert result.exit_code == 0 + assert "Deleting project Old Project" in result.output + assert "Project name: " not in result.output + + +def test_help(): + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "--project-name TEXT" in result.output + assert "[required]" in result.output + + +def test_script(): + result = subprocess.run( + ["coverage", "run", mod.__file__, "--help"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_options/test_tutorial006.py b/tests/test_tutorial/test_options/test_tutorial006.py new file mode 100644 index 0000000000..df08073429 --- /dev/null +++ b/tests/test_tutorial/test_options/test_tutorial006.py @@ -0,0 +1,34 @@ +import subprocess + +import typer +from typer.testing import CliRunner + +from options import tutorial006 as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_call(): + result = runner.invoke(app) + assert result.exit_code == 0 + assert "Hello Wade Wilson" in result.output + + +def test_help(): + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "--fullname TEXT" in result.output + assert "[default: Wade Wilson]" in result.output + + +def test_script(): + result = subprocess.run( + ["coverage", "run", mod.__file__, "--help"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/typer/completion.py b/typer/completion.py index bb7cba6a3a..586cc79817 100644 --- a/typer/completion.py +++ b/typer/completion.py @@ -14,7 +14,8 @@ def install_callback(ctx: click.Context, param: click.Parameter, value: Any) -> if not value or ctx.resilient_parsing: return value shell, path = click_completion.core.install() - click.echo(f"{shell} completion installed in {path}") + click.secho(f"{shell} completion installed in {path}.", fg="green") + click.echo("Completion will take effect once you restart the terminal.") sys.exit(0) diff --git a/typer/main.py b/typer/main.py index 78e510da9d..16231b5690 100644 --- a/typer/main.py +++ b/typer/main.py @@ -28,7 +28,7 @@ try: import click_completion from .completion import _install_completion_placeholder_function -except ImportError: +except ImportError: # pragma: no cover click_completion = None diff --git a/typer/models.py b/typer/models.py index a91a6de0b2..2e227fca3d 100644 --- a/typer/models.py +++ b/typer/models.py @@ -14,7 +14,7 @@ import click -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: no cover from .main import Typer # noqa