@@ -721,6 +719,9 @@ You just have to pass it the module to import (`rick_portal_gun.main`) and it wi
By specifying the `--name` of the program it will be able to use it while generating the docs.
+!!! tip
+ If you installed `typer-slim` and don't have the `typer` command, you can use `python -m typer` instead.
+
### Publish a new version with the docs
Now you can publish a new version with the updated docs.
diff --git a/docs/tutorial/printing.md b/docs/tutorial/printing.md
index 120c0facbc..058998473e 100644
--- a/docs/tutorial/printing.md
+++ b/docs/tutorial/printing.md
@@ -18,27 +18,7 @@ Hello World
## Use Rich
-You can also display beautiful and more complex information using
-
-```console
-// Rich comes with typer[all]
-$ pip install "typer[all]"
----> 100%
-Successfully installed typer rich
-
-// Alternatively, you can install Rich independently
-$ pip install rich
----> 100%
-Successfully installed rich
-```
-
-
. It comes by default when you install `typer`.
### Use Rich `print`
diff --git a/docs/typer-cli.md b/docs/tutorial/typer-command.md
similarity index 61%
rename from docs/typer-cli.md
rename to docs/tutorial/typer-command.md
index 35f17e4d77..710115a749 100644
--- a/docs/typer-cli.md
+++ b/docs/tutorial/typer-command.md
@@ -1,100 +1,45 @@
-# Typer CLI
+# `typer` command
-
-
-There is an optional utility tool called **Typer CLI**, additional to **Typer** itself.
-
-It's main feature is to provide ✨ completion ✨ in the Terminal for your own small programs built with **Typer**.
-
-...without you having to create a complete installable Python package.
+The `typer` command provides ✨ completion ✨ in the Terminal for your own small scripts. Even if they don't use Typer internally. Of course, it works better if you use **Typer** in your script.
It's probably most useful if you have a small custom Python script using **Typer** (maybe as part of some project), for some small tasks, and it's not complex/important enough to create a whole installable Python package for it (something to be installed with `pip`).
-In that case, you can install **Typer CLI**, and run your program with the `typer` command in your Terminal, and it will provide completion for your script.
-
-You can also use **Typer CLI** to generate Markdown documentation for your own **Typer** programs 📝.
-
----
-
-**Documentation**:
-
----
-
-## **Typer** or **Typer CLI**
-
-**Typer** is a library for building CLIs (Command Line Interface applications).
-
-You use **Typer** in your Python scripts. Like in:
-
-```Python
-import typer
+In that case, you can run your program with the `typer` command in your Terminal, and it will provide completion for your script.
+The `typer` command also has functionality to generate Markdown documentation for your own **Typer** programs 📝.
-def main():
- typer.echo("Hello World")
+## Install
+When you install **Typer** with:
-if __name__ == "__main__":
- typer.run(main)
+```bash
+pip install typer
```
-**Typer CLI** is a command line application to run simple programs created with **Typer**, with completion in your terminal 🚀.
+...it includes the `typer` command.
-You use **Typer CLI** in your terminal, to run your scripts (as an alternative to calling `python` directly). Like in:
+If you don't want to have the `typer` command, you can install instead:
-
-
-```console
-$ typer my_script.py run
-
-Hello World
+```bash
+pip install typer-slim
```
-
-
-But you never import anything from **Typer CLI** in your own scripts.
-
-## Usage
-
-### Install
+You can still use it by calling the Typer library as a module with:
-Install **Typer CLI**:
-
-
-
-```console
-$ python -m pip install typer-cli
----> 100%
-Successfully installed typer-cli
+```bash
+python -m typer
```
-
-
-That creates a `typer` command you can call in your terminal, much like `python`, `git`, or `echo`.
+## Install completion
-You can then install completion for it:
+You can then install completion for the `typer` command with:
```console
$ typer --install-completion
-zsh completion installed in /home/user/.bashrc.
+bash completion installed in /home/user/.bashrc.
Completion will take effect once you restart the terminal.
```
@@ -170,11 +115,9 @@ There's nothing wrong with using Python directly to run it. And, in fact, if som
⛔️ But in your terminal, you won't get completion when hitting
TAB for any of the subcommands or options, like `hello`, `bye`, and `--name`.
-### Run with **Typer CLI**
-
-Here's where **Typer CLI** is useful.
+### Run with the `typer` command.
-You can also run the same script with the `typer` command you get after installing `typer-cli`:
+You can also run the same script with the `typer` command:
@@ -197,24 +140,24 @@ Bye Camila
* Instead of using `python` directly you use the `typer` command.
* After the name of the file, add the subcommand `run`.
-✔️ If you installed completion for **Typer CLI** (for the `typer` command) as described above, when you hit TAB you will have ✨ completion for everything ✨, including all the subcommands and options of your script, like `hello`, `bye`, and `--name` 🚀.
+✔️ If you installed completion for the `typer` command as described above, when you hit TAB you will have ✨ completion for everything ✨, including all the subcommands and options of your script, like `hello`, `bye`, and `--name` 🚀.
## If main
-Because **Typer CLI** won't use the block with:
+Because the `typer` command won't use the block with:
```Python
if __name__ == "__main__":
app()
```
-...you can also remove it if you are calling that script only with **Typer CLI** (using the `typer` command).
+...you can also remove it if you are calling that script only with the `typer` command.
## Run other files
-**Typer CLI** can run any script with **Typer**, but the script doesn't even have to use **Typer** at all.
+The `typer` command can run any script with **Typer**, but the script doesn't even have to use **Typer** at all.
-**Typer CLI** could even run a file with a function that could be used with `typer.run()`, even if the script doesn't use `typer.run()` or anything else.
+You could even run a file with a function that could be used with `typer.run()`, even if the script doesn't use `typer.run()` or anything else.
For example, a file `main.py` like this will still work:
@@ -281,7 +224,7 @@ You can specify one of the following **CLI options**:
### Defaults
-When your run a script with the **Typer CLI** (the `typer` command) it will use the app from the following priority:
+When your run a script with the `typer` command it will use the app from the following priority:
* An app object from the `--app` *CLI Option*.
* A function to convert to a **Typer** app from `--func` *CLI Option* (like when using `typer.run()`).
@@ -292,7 +235,7 @@ When your run a script with the **Typer CLI** (the `typer` command) it will use
## Generate docs
-**Typer CLI** can also generate Markdown documentation for your **Typer** application.
+You can also use the `typer` command to generate Markdown documentation for your **Typer** application.
### Sample script with docs
@@ -302,9 +245,9 @@ For example, you could have a script like:
{!../docs_src/commands/help/tutorial001.py!}
```
-### Generate docs with Typer CLI
+### Generate docs with the `typer` command
-Then you could generate docs for it with **Typer CLI**.
+Then you could generate docs for it with the `typer` command.
You can use the subcommand `utils`.
@@ -318,6 +261,13 @@ $ typer some_script.py utils docs
+!!! tip
+ If you installed only `typer-slim` and you don't have the `typer` command, you can still generate docs with:
+
+ ```console
+ $ python -m typer some_script.py utils docs
+ ```
+
**Options**:
* `--name TEXT`: The name of the CLI program to use in docs.
@@ -425,9 +375,3 @@ $ awesome-cli init [OPTIONS]
**Options**:
* `--help`: Show this message and exit.
-
----
-
-## License
-
-**Typer CLI**, the same as **Typer**, is licensed under the terms of the MIT license.
diff --git a/mkdocs.insiders.yml b/mkdocs.insiders.yml
index 7c1870afb7..d0b5a56f6e 100644
--- a/mkdocs.insiders.yml
+++ b/mkdocs.insiders.yml
@@ -1,5 +1,4 @@
INHERIT: mkdocs.yml
-plugins:
- - search
+# plugins:
# TODO: Re-enable once this is fixed: https://github.com/squidfunk/mkdocs-material/issues/6983
# - social
diff --git a/mkdocs.yml b/mkdocs.yml
index 8022a19589..77024d83bb 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -16,6 +16,11 @@ theme:
repo_name: tiangolo/typer
repo_url: https://github.com/tiangolo/typer
edit_uri: ""
+plugins:
+ search: null
+ redirects:
+ redirect_maps:
+ typer-cli.md: tutorial/typer-command.md
nav:
- Typer: index.md
@@ -81,7 +86,7 @@ nav:
- Using Click: tutorial/using-click.md
- Building a Package: tutorial/package.md
- tutorial/exceptions.md
- - Typer CLI - completion for small scripts: typer-cli.md
+ - tutorial/typer-command.md
- Alternatives, Inspiration and Comparisons: alternatives.md
- Help Typer - Get Help: help-typer.md
- Development - Contributing: contributing.md
diff --git a/pyproject.toml b/pyproject.toml
index 2ae5002fc8..f5abe95d3c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -3,7 +3,9 @@ requires = ["pdm-backend"]
build-backend = "pdm.backend"
[project]
-name = "typer"
+# pip install typer-slim: is only the Typer library, without extras
+# pip install typer: is typer-slim[standard], with extras and typer-cli
+name = "typer-slim"
dynamic = ["version"]
description = "Typer, build great CLIs. Easy to code. Based on Python type hints."
authors = [
@@ -43,9 +45,12 @@ homepage = "https://github.com/tiangolo/typer"
[project.optional-dependencies]
all = [
- "colorama >=0.4.3,<0.5.0",
- "shellingham >=1.3.0,<2.0.0",
- "rich >=10.11.0,<14.0.0",
+ "shellingham >=1.3.0",
+ "rich >=10.11.0",
+]
+standard = [
+ "shellingham >=1.3.0",
+ "rich >=10.11.0",
]
[tool.pdm]
diff --git a/tests/assets/cli/__init__.py b/tests/assets/cli/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tests/assets/cli/app_other_name.py b/tests/assets/cli/app_other_name.py
new file mode 100644
index 0000000000..78bb101a90
--- /dev/null
+++ b/tests/assets/cli/app_other_name.py
@@ -0,0 +1,8 @@
+import typer
+
+application = typer.Typer()
+
+
+@application.command()
+def callback(name: str = "World"):
+ typer.echo(f"Hello {name}")
diff --git a/tests/assets/cli/empty_script.py b/tests/assets/cli/empty_script.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tests/assets/cli/func_other_name.py b/tests/assets/cli/func_other_name.py
new file mode 100644
index 0000000000..27a13823ef
--- /dev/null
+++ b/tests/assets/cli/func_other_name.py
@@ -0,0 +1,2 @@
+def some_function(name: str = "World"):
+ print(f"Hello {name}")
diff --git a/tests/assets/cli/multi_app.py b/tests/assets/cli/multi_app.py
new file mode 100644
index 0000000000..1442aa2442
--- /dev/null
+++ b/tests/assets/cli/multi_app.py
@@ -0,0 +1,40 @@
+import typer
+
+sub_app = typer.Typer()
+
+variable = "Some text"
+
+
+@sub_app.command()
+def hello(name: str = "World", age: int = typer.Option(0, help="The age of the user")):
+ """
+ Say Hello
+ """
+ typer.echo(f"Hello {name}")
+
+
+@sub_app.command()
+def hi(user: str = typer.Argument("World", help="The name of the user to greet")):
+ """
+ Say Hi
+ """
+
+
+@sub_app.command()
+def bye():
+ """
+ Say bye
+ """
+ typer.echo("sub bye")
+
+
+app = typer.Typer(help="Demo App", epilog="The end")
+app.add_typer(sub_app, name="sub")
+
+
+@app.command()
+def top():
+ """
+ Top command
+ """
+ typer.echo("top")
diff --git a/tests/assets/cli/multi_app_cli.py b/tests/assets/cli/multi_app_cli.py
new file mode 100644
index 0000000000..0a73c7ebc3
--- /dev/null
+++ b/tests/assets/cli/multi_app_cli.py
@@ -0,0 +1,22 @@
+import typer
+
+sub_app = typer.Typer()
+
+
+@sub_app.command()
+def hello():
+ typer.echo("sub hello")
+
+
+@sub_app.command()
+def bye():
+ typer.echo("sub bye")
+
+
+cli = typer.Typer()
+cli.add_typer(sub_app, name="sub")
+
+
+@cli.command()
+def top():
+ typer.echo("top")
diff --git a/tests/assets/cli/multi_func.py b/tests/assets/cli/multi_func.py
new file mode 100644
index 0000000000..c272ee2be1
--- /dev/null
+++ b/tests/assets/cli/multi_func.py
@@ -0,0 +1,12 @@
+message = "Stuff"
+
+
+def say_stuff():
+ print(message)
+
+
+def main(name: str = "World"):
+ """
+ Say hi to someone, by default to the World.
+ """
+ print(f"Hello {name}")
diff --git a/tests/assets/cli/multiapp-docs.md b/tests/assets/cli/multiapp-docs.md
new file mode 100644
index 0000000000..ed4592f5c8
--- /dev/null
+++ b/tests/assets/cli/multiapp-docs.md
@@ -0,0 +1,102 @@
+# `multiapp`
+
+Demo App
+
+**Usage**:
+
+```console
+$ multiapp [OPTIONS] COMMAND [ARGS]...
+```
+
+**Options**:
+
+* `--install-completion`: Install completion for the current shell.
+* `--show-completion`: Show completion for the current shell, to copy it or customize the installation.
+* `--help`: Show this message and exit.
+
+The end
+
+**Commands**:
+
+* `sub`
+* `top`: Top command
+
+## `multiapp sub`
+
+**Usage**:
+
+```console
+$ multiapp sub [OPTIONS] COMMAND [ARGS]...
+```
+
+**Options**:
+
+* `--help`: Show this message and exit.
+
+**Commands**:
+
+* `bye`: Say bye
+* `hello`: Say Hello
+* `hi`: Say Hi
+
+### `multiapp sub bye`
+
+Say bye
+
+**Usage**:
+
+```console
+$ multiapp sub bye [OPTIONS]
+```
+
+**Options**:
+
+* `--help`: Show this message and exit.
+
+### `multiapp sub hello`
+
+Say Hello
+
+**Usage**:
+
+```console
+$ multiapp sub hello [OPTIONS]
+```
+
+**Options**:
+
+* `--name TEXT`: [default: World]
+* `--age INTEGER`: The age of the user [default: 0]
+* `--help`: Show this message and exit.
+
+### `multiapp sub hi`
+
+Say Hi
+
+**Usage**:
+
+```console
+$ multiapp sub hi [OPTIONS] [USER]
+```
+
+**Arguments**:
+
+* `[USER]`: The name of the user to greet [default: World]
+
+**Options**:
+
+* `--help`: Show this message and exit.
+
+## `multiapp top`
+
+Top command
+
+**Usage**:
+
+```console
+$ multiapp top [OPTIONS]
+```
+
+**Options**:
+
+* `--help`: Show this message and exit.
diff --git a/tests/assets/cli/not_python.txt b/tests/assets/cli/not_python.txt
new file mode 100644
index 0000000000..5eb1a5ddd5
--- /dev/null
+++ b/tests/assets/cli/not_python.txt
@@ -0,0 +1 @@
+This is not Python
diff --git a/tests/assets/cli/sample.py b/tests/assets/cli/sample.py
new file mode 100644
index 0000000000..79ff29b5ac
--- /dev/null
+++ b/tests/assets/cli/sample.py
@@ -0,0 +1,25 @@
+import typer
+
+app = typer.Typer()
+
+
+@app.command()
+def hello(name: str = "World", formal: bool = False):
+ """
+ Say hi
+ """
+ if formal:
+ typer.echo(f"Good morning Ms. {name}")
+ else:
+ typer.echo(f"Hello {name}!")
+
+
+@app.command()
+def bye(friend: bool = False):
+ """
+ Say bye
+ """
+ if friend:
+ typer.echo("Goodbye my friend")
+ else:
+ typer.echo("Goodbye")
diff --git a/tests/test_cli/__init__.py b/tests/test_cli/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tests/test_cli/test_app_other_name.py b/tests/test_cli/test_app_other_name.py
new file mode 100644
index 0000000000..2811c4376f
--- /dev/null
+++ b/tests/test_cli/test_app_other_name.py
@@ -0,0 +1,41 @@
+import subprocess
+import sys
+
+
+def test_script_help():
+ result = subprocess.run(
+ [
+ sys.executable,
+ "-m",
+ "coverage",
+ "run",
+ "-m",
+ "typer",
+ "tests/assets/cli/app_other_name.py",
+ "run",
+ "--help",
+ ],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "--name" in result.stdout
+
+
+def test_script():
+ result = subprocess.run(
+ [
+ sys.executable,
+ "-m",
+ "coverage",
+ "run",
+ "-m",
+ "typer",
+ "tests/assets/cli/app_other_name.py",
+ "run",
+ "--name",
+ "Camila",
+ ],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "Hello Camila" in result.stdout
diff --git a/tests/test_cli/test_completion_run.py b/tests/test_cli/test_completion_run.py
new file mode 100644
index 0000000000..20695b9339
--- /dev/null
+++ b/tests/test_cli/test_completion_run.py
@@ -0,0 +1,19 @@
+import os
+import subprocess
+import sys
+
+
+def test_script_completion_run():
+ result = subprocess.run(
+ [sys.executable, "-m", "coverage", "run", "-m", "typer"],
+ capture_output=True,
+ encoding="utf-8",
+ env={
+ **os.environ,
+ "___MAIN__.PY_COMPLETE": "complete_bash",
+ "_PYTHON _M TYPER_COMPLETE": "complete_bash",
+ "COMP_WORDS": "typer tests/assets/cli/sample.py",
+ "COMP_CWORD": "2",
+ },
+ )
+ assert "run" in result.stdout
diff --git a/tests/test_cli/test_doc.py b/tests/test_cli/test_doc.py
new file mode 100644
index 0000000000..903c51ddec
--- /dev/null
+++ b/tests/test_cli/test_doc.py
@@ -0,0 +1,112 @@
+import subprocess
+import sys
+from pathlib import Path
+
+
+def test_doc():
+ result = subprocess.run(
+ [
+ sys.executable,
+ "-m",
+ "coverage",
+ "run",
+ "-m",
+ "typer",
+ "tests.assets.cli.multi_app",
+ "utils",
+ "docs",
+ "--name",
+ "multiapp",
+ ],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ docs_path: Path = Path(__file__).parent.parent / "assets/cli/multiapp-docs.md"
+ docs = docs_path.read_text()
+ assert docs in result.stdout
+ assert "**Arguments**" in result.stdout
+
+
+def test_doc_output(tmp_path: Path):
+ out_file: Path = tmp_path / "out.md"
+ result = subprocess.run(
+ [
+ sys.executable,
+ "-m",
+ "coverage",
+ "run",
+ "-m",
+ "typer",
+ "tests.assets.cli.multi_app",
+ "utils",
+ "docs",
+ "--name",
+ "multiapp",
+ "--output",
+ str(out_file),
+ ],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ docs_path: Path = Path(__file__).parent.parent / "assets/cli/multiapp-docs.md"
+ docs = docs_path.read_text()
+ written_docs = out_file.read_text()
+ assert docs in written_docs
+ assert "Docs saved to:" in result.stdout
+
+
+def test_doc_not_existing():
+ result = subprocess.run(
+ [
+ sys.executable,
+ "-m",
+ "coverage",
+ "run",
+ "-m",
+ "typer",
+ "no_typer",
+ "utils",
+ "docs",
+ ],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "Could not import as Python module:" in result.stderr
+
+
+def test_doc_no_typer():
+ result = subprocess.run(
+ [
+ sys.executable,
+ "-m",
+ "coverage",
+ "run",
+ "-m",
+ "typer",
+ "tests/assets/cli/empty_script.py",
+ "utils",
+ "docs",
+ ],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "No Typer app found" in result.stderr
+
+
+def test_doc_file_not_existing():
+ result = subprocess.run(
+ [
+ sys.executable,
+ "-m",
+ "coverage",
+ "run",
+ "-m",
+ "typer",
+ "assets/cli/not_existing.py",
+ "utils",
+ "docs",
+ ],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "Not a valid file or Python module:" in result.stderr
diff --git a/tests/test_cli/test_empty_script.py b/tests/test_cli/test_empty_script.py
new file mode 100644
index 0000000000..7e90789eb4
--- /dev/null
+++ b/tests/test_cli/test_empty_script.py
@@ -0,0 +1,20 @@
+import subprocess
+import sys
+
+
+def test_script_help():
+ result = subprocess.run(
+ [
+ sys.executable,
+ "-m",
+ "coverage",
+ "run",
+ "-m",
+ "typer",
+ "tests/assets/cli/empty_script.py",
+ "--help",
+ ],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "run" not in result.stdout
diff --git a/tests/test_cli/test_func_other_name.py b/tests/test_cli/test_func_other_name.py
new file mode 100644
index 0000000000..7949ff8f9b
--- /dev/null
+++ b/tests/test_cli/test_func_other_name.py
@@ -0,0 +1,22 @@
+import subprocess
+import sys
+
+
+def test_script():
+ result = subprocess.run(
+ [
+ sys.executable,
+ "-m",
+ "coverage",
+ "run",
+ "-m",
+ "typer",
+ "tests/assets/cli/func_other_name.py",
+ "run",
+ "--name",
+ "Camila",
+ ],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "Hello Camila" in result.stdout
diff --git a/tests/test_cli/test_help.py b/tests/test_cli/test_help.py
new file mode 100644
index 0000000000..64e5495c9a
--- /dev/null
+++ b/tests/test_cli/test_help.py
@@ -0,0 +1,38 @@
+import subprocess
+import sys
+
+
+def test_script_help():
+ result = subprocess.run(
+ [
+ sys.executable,
+ "-m",
+ "coverage",
+ "run",
+ "-m",
+ "typer",
+ "tests/assets/cli/sample.py",
+ "--help",
+ ],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "run" in result.stdout
+
+
+def test_not_python():
+ result = subprocess.run(
+ [
+ sys.executable,
+ "-m",
+ "coverage",
+ "run",
+ "-m",
+ "typer",
+ "tests/assets/cli/not_python.txt",
+ "run",
+ ],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "Could not import as Python file" in result.stderr
diff --git a/tests/test_cli/test_multi_app.py b/tests/test_cli/test_multi_app.py
new file mode 100644
index 0000000000..5315289f5e
--- /dev/null
+++ b/tests/test_cli/test_multi_app.py
@@ -0,0 +1,123 @@
+import subprocess
+import sys
+
+
+def test_script_help():
+ result = subprocess.run(
+ [
+ sys.executable,
+ "-m",
+ "coverage",
+ "run",
+ "-m",
+ "typer",
+ "tests/assets/cli/multi_app.py",
+ "run",
+ "--help",
+ ],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "sub" in result.stdout
+ assert "top" in result.stdout
+
+
+def test_script_app_non_existent():
+ result = subprocess.run(
+ [
+ sys.executable,
+ "-m",
+ "coverage",
+ "run",
+ "-m",
+ "typer",
+ "--app",
+ "non_existent",
+ "tests/assets/cli/multi_app.py",
+ "run",
+ "--help",
+ ],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "Not a Typer object:" in result.stderr
+
+
+def test_script_sub():
+ result = subprocess.run(
+ [
+ sys.executable,
+ "-m",
+ "coverage",
+ "run",
+ "-m",
+ "typer",
+ "tests/assets/cli/multi_app.py",
+ "run",
+ "sub",
+ "--help",
+ ],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "bye" in result.stdout
+ assert "hello" in result.stdout
+
+
+def test_script_top():
+ result = subprocess.run(
+ [
+ sys.executable,
+ "-m",
+ "coverage",
+ "run",
+ "-m",
+ "typer",
+ "tests/assets/cli/multi_app.py",
+ "run",
+ "top",
+ ],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "top" in result.stdout
+
+
+def test_script_sub_hello():
+ result = subprocess.run(
+ [
+ sys.executable,
+ "-m",
+ "coverage",
+ "run",
+ "-m",
+ "typer",
+ "tests/assets/cli/multi_app.py",
+ "run",
+ "sub",
+ "hello",
+ ],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "Hello World" in result.stdout
+
+
+def test_script_sub_bye():
+ result = subprocess.run(
+ [
+ sys.executable,
+ "-m",
+ "coverage",
+ "run",
+ "-m",
+ "typer",
+ "tests/assets/cli/multi_app.py",
+ "run",
+ "sub",
+ "bye",
+ ],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "sub bye" in result.stdout
diff --git a/tests/test_cli/test_multi_app_cli.py b/tests/test_cli/test_multi_app_cli.py
new file mode 100644
index 0000000000..1eaddaba87
--- /dev/null
+++ b/tests/test_cli/test_multi_app_cli.py
@@ -0,0 +1,102 @@
+import subprocess
+import sys
+
+
+def test_script_help():
+ result = subprocess.run(
+ [
+ sys.executable,
+ "-m",
+ "coverage",
+ "run",
+ "-m",
+ "typer",
+ "tests/assets/cli/multi_app_cli.py",
+ "run",
+ "--help",
+ ],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "sub" in result.stdout
+ assert "top" in result.stdout
+
+
+def test_script_sub():
+ result = subprocess.run(
+ [
+ sys.executable,
+ "-m",
+ "coverage",
+ "run",
+ "-m",
+ "typer",
+ "tests/assets/cli/multi_app_cli.py",
+ "run",
+ "sub",
+ "--help",
+ ],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "bye" in result.stdout
+ assert "hello" in result.stdout
+
+
+def test_script_top():
+ result = subprocess.run(
+ [
+ sys.executable,
+ "-m",
+ "coverage",
+ "run",
+ "-m",
+ "typer",
+ "tests/assets/cli/multi_app_cli.py",
+ "run",
+ "top",
+ ],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "top" in result.stdout
+
+
+def test_script_sub_hello():
+ result = subprocess.run(
+ [
+ sys.executable,
+ "-m",
+ "coverage",
+ "run",
+ "-m",
+ "typer",
+ "tests/assets/cli/multi_app_cli.py",
+ "run",
+ "sub",
+ "hello",
+ ],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "sub hello" in result.stdout
+
+
+def test_script_sub_bye():
+ result = subprocess.run(
+ [
+ sys.executable,
+ "-m",
+ "coverage",
+ "run",
+ "-m",
+ "typer",
+ "tests/assets/cli/multi_app_cli.py",
+ "run",
+ "sub",
+ "bye",
+ ],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "sub bye" in result.stdout
diff --git a/tests/test_cli/test_multi_app_sub.py b/tests/test_cli/test_multi_app_sub.py
new file mode 100644
index 0000000000..7d5f6f6888
--- /dev/null
+++ b/tests/test_cli/test_multi_app_sub.py
@@ -0,0 +1,46 @@
+import subprocess
+import sys
+
+
+def test_script_help():
+ result = subprocess.run(
+ [
+ sys.executable,
+ "-m",
+ "coverage",
+ "run",
+ "-m",
+ "typer",
+ "--app",
+ "sub_app",
+ "tests/assets/cli/multi_app.py",
+ "run",
+ "--help",
+ ],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "bye" in result.stdout
+ assert "hello" in result.stdout
+ assert "top" not in result.stdout
+
+
+def test_script():
+ result = subprocess.run(
+ [
+ sys.executable,
+ "-m",
+ "coverage",
+ "run",
+ "-m",
+ "typer",
+ "--app",
+ "sub_app",
+ "tests/assets/cli/multi_app.py",
+ "run",
+ "hello",
+ ],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "Hello World" in result.stdout
diff --git a/tests/test_cli/test_multi_func.py b/tests/test_cli/test_multi_func.py
new file mode 100644
index 0000000000..8b4b43a94d
--- /dev/null
+++ b/tests/test_cli/test_multi_func.py
@@ -0,0 +1,106 @@
+import subprocess
+import sys
+
+
+def test_help():
+ result = subprocess.run(
+ [
+ sys.executable,
+ "-m",
+ "coverage",
+ "run",
+ "-m",
+ "typer",
+ "tests/assets/cli/multi_func.py",
+ "run",
+ "--help",
+ ],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "Say hi to someone, by default to the World." in result.stdout
+
+
+def test_script():
+ result = subprocess.run(
+ [
+ sys.executable,
+ "-m",
+ "coverage",
+ "run",
+ "-m",
+ "typer",
+ "tests/assets/cli/multi_func.py",
+ "run",
+ "--name",
+ "Camila",
+ ],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "Hello Camila" in result.stdout
+
+
+def test_script_func_non_existent():
+ result = subprocess.run(
+ [
+ sys.executable,
+ "-m",
+ "coverage",
+ "run",
+ "-m",
+ "typer",
+ "--func",
+ "non_existent",
+ "tests/assets/cli/multi_func.py",
+ "run",
+ "--name",
+ "Camila",
+ ],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "Not a function:" in result.stderr
+
+
+def test_script_func_not_function():
+ result = subprocess.run(
+ [
+ sys.executable,
+ "-m",
+ "coverage",
+ "run",
+ "-m",
+ "typer",
+ "--func",
+ "message",
+ "tests/assets/cli/multi_func.py",
+ "run",
+ "--name",
+ "Camila",
+ ],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "Not a function:" in result.stderr
+
+
+def test_script_func():
+ result = subprocess.run(
+ [
+ sys.executable,
+ "-m",
+ "coverage",
+ "run",
+ "-m",
+ "typer",
+ "--func",
+ "say_stuff",
+ "tests/assets/cli/multi_func.py",
+ "run",
+ ],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "Hello" not in result.stdout
+ assert "Stuff" in result.stdout
diff --git a/tests/test_cli/test_not_python.py b/tests/test_cli/test_not_python.py
new file mode 100644
index 0000000000..00384d1f55
--- /dev/null
+++ b/tests/test_cli/test_not_python.py
@@ -0,0 +1,20 @@
+import subprocess
+import sys
+
+
+def test_not_python():
+ result = subprocess.run(
+ [
+ sys.executable,
+ "-m",
+ "coverage",
+ "run",
+ "-m",
+ "typer",
+ "tests/assets/cli/not_python.txt",
+ "run",
+ ],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "Could not import as Python file" in result.stderr
diff --git a/tests/test_cli/test_sub.py b/tests/test_cli/test_sub.py
new file mode 100644
index 0000000000..6eb0d4dac4
--- /dev/null
+++ b/tests/test_cli/test_sub.py
@@ -0,0 +1,139 @@
+import subprocess
+import sys
+
+
+def test_script_hello():
+ result = subprocess.run(
+ [
+ sys.executable,
+ "-m",
+ "coverage",
+ "run",
+ "-m",
+ "typer",
+ "tests/assets/cli/sample.py",
+ "run",
+ "hello",
+ ],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "Hello World!" in result.stdout
+
+
+def test_script_hello_name():
+ result = subprocess.run(
+ [
+ sys.executable,
+ "-m",
+ "coverage",
+ "run",
+ "-m",
+ "typer",
+ "tests/assets/cli/sample.py",
+ "run",
+ "hello",
+ "--name",
+ "Camila",
+ ],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "Hello Camila!" in result.stdout
+
+
+def test_script_hello_name_formal():
+ result = subprocess.run(
+ [
+ sys.executable,
+ "-m",
+ "coverage",
+ "run",
+ "-m",
+ "typer",
+ "tests/assets/cli/sample.py",
+ "run",
+ "hello",
+ "--name",
+ "Camila",
+ "--formal",
+ ],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "Good morning Ms. Camila" in result.stdout
+
+
+def test_script_bye():
+ result = subprocess.run(
+ [
+ sys.executable,
+ "-m",
+ "coverage",
+ "run",
+ "-m",
+ "typer",
+ "tests/assets/cli/sample.py",
+ "run",
+ "bye",
+ ],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "Goodbye" in result.stdout
+
+
+def test_script_bye_friend():
+ result = subprocess.run(
+ [
+ sys.executable,
+ "-m",
+ "coverage",
+ "run",
+ "-m",
+ "typer",
+ "tests/assets/cli/sample.py",
+ "run",
+ "bye",
+ "--friend",
+ ],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "Goodbye my friend" in result.stdout
+
+
+def test_script_help():
+ result = subprocess.run(
+ [
+ sys.executable,
+ "-m",
+ "coverage",
+ "run",
+ "-m",
+ "typer",
+ "tests/assets/cli/sample.py",
+ "--help",
+ ],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "run" in result.stdout
+
+
+def test_not_python():
+ result = subprocess.run(
+ [
+ sys.executable,
+ "-m",
+ "coverage",
+ "run",
+ "-m",
+ "typer",
+ "tests/assets/cli/not_python.txt",
+ "run",
+ ],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "Could not import as Python file" in result.stderr
diff --git a/tests/test_cli/test_sub_completion.py b/tests/test_cli/test_sub_completion.py
new file mode 100644
index 0000000000..0658883733
--- /dev/null
+++ b/tests/test_cli/test_sub_completion.py
@@ -0,0 +1,19 @@
+import os
+import subprocess
+import sys
+
+
+def test_script_completion_run():
+ result = subprocess.run(
+ [sys.executable, "-m", "coverage", "run", "-m", "typer"],
+ capture_output=True,
+ encoding="utf-8",
+ env={
+ **os.environ,
+ "___MAIN__.PY_COMPLETE": "complete_bash",
+ "_PYTHON _M TYPER_COMPLETE": "complete_bash",
+ "COMP_WORDS": "typer tests/assets/cli/sample.py run hello --",
+ "COMP_CWORD": "4",
+ },
+ )
+ assert "--name" in result.stdout
diff --git a/tests/test_cli/test_sub_help.py b/tests/test_cli/test_sub_help.py
new file mode 100644
index 0000000000..d59f56fb2c
--- /dev/null
+++ b/tests/test_cli/test_sub_help.py
@@ -0,0 +1,24 @@
+import subprocess
+import sys
+
+
+def test_script_help():
+ result = subprocess.run(
+ [
+ sys.executable,
+ "-m",
+ "coverage",
+ "run",
+ "-m",
+ "typer",
+ "tests/assets/cli/sample.py",
+ "run",
+ "--help",
+ ],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "bye" in result.stdout
+ assert "Say bye" in result.stdout
+ assert "hello" in result.stdout
+ assert "Say hi" in result.stdout
diff --git a/tests/test_cli/test_version.py b/tests/test_cli/test_version.py
new file mode 100644
index 0000000000..18f4b00efe
--- /dev/null
+++ b/tests/test_cli/test_version.py
@@ -0,0 +1,11 @@
+import subprocess
+import sys
+
+
+def test_script_help():
+ result = subprocess.run(
+ [sys.executable, "-m", "coverage", "run", "-m", "typer", "--version"],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "Typer version:" in result.stdout
diff --git a/tests/test_compat/test_option_get_help.py b/tests/test_compat/test_option_get_help.py
index baca132cf0..362298b99a 100644
--- a/tests/test_compat/test_option_get_help.py
+++ b/tests/test_compat/test_option_get_help.py
@@ -48,7 +48,6 @@ def test_completion():
**os.environ,
"_COMPAT_CLICK7_8.PY_COMPLETE": "complete_zsh",
"_TYPER_COMPLETE_ARGS": "compat_click7_8.py --nickname ",
- "_TYPER_COMPLETE_TESTING": "True",
},
)
assert "Jonny" in result.stdout
diff --git a/tests/test_completion/test_completion.py b/tests/test_completion/test_completion.py
index 9f5dd426db..2af209e2dc 100644
--- a/tests/test_completion/test_completion.py
+++ b/tests/test_completion/test_completion.py
@@ -51,7 +51,6 @@ def test_completion_invalid_instruction():
env={
**os.environ,
"_TUTORIAL001.PY_COMPLETE": "sourcebash",
- "_TYPER_COMPLETE_TESTING": "True",
},
)
assert result.returncode != 0
@@ -66,7 +65,6 @@ def test_completion_source_bash():
env={
**os.environ,
"_TUTORIAL001.PY_COMPLETE": "source_bash",
- "_TYPER_COMPLETE_TESTING": "True",
},
)
assert (
@@ -83,7 +81,6 @@ def test_completion_source_invalid_shell():
env={
**os.environ,
"_TUTORIAL001.PY_COMPLETE": "source_xxx",
- "_TYPER_COMPLETE_TESTING": "True",
},
)
assert "Shell xxx not supported." in result.stderr
@@ -97,7 +94,6 @@ def test_completion_source_invalid_instruction():
env={
**os.environ,
"_TUTORIAL001.PY_COMPLETE": "explode_bash",
- "_TYPER_COMPLETE_TESTING": "True",
},
)
assert 'Completion instruction "explode" not supported.' in result.stderr
@@ -111,7 +107,6 @@ def test_completion_source_zsh():
env={
**os.environ,
"_TUTORIAL001.PY_COMPLETE": "source_zsh",
- "_TYPER_COMPLETE_TESTING": "True",
},
)
assert "compdef _tutorial001py_completion tutorial001.py" in result.stdout
@@ -125,7 +120,6 @@ def test_completion_source_fish():
env={
**os.environ,
"_TUTORIAL001.PY_COMPLETE": "source_fish",
- "_TYPER_COMPLETE_TESTING": "True",
},
)
assert "complete --command tutorial001.py --no-files" in result.stdout
@@ -139,7 +133,6 @@ def test_completion_source_powershell():
env={
**os.environ,
"_TUTORIAL001.PY_COMPLETE": "source_powershell",
- "_TYPER_COMPLETE_TESTING": "True",
},
)
assert (
@@ -156,7 +149,6 @@ def test_completion_source_pwsh():
env={
**os.environ,
"_TUTORIAL001.PY_COMPLETE": "source_pwsh",
- "_TYPER_COMPLETE_TESTING": "True",
},
)
assert (
diff --git a/tests/test_completion/test_completion_complete.py b/tests/test_completion/test_completion_complete.py
index 359768f859..ea37f68546 100644
--- a/tests/test_completion/test_completion_complete.py
+++ b/tests/test_completion/test_completion_complete.py
@@ -15,7 +15,6 @@ def test_completion_complete_subcommand_bash():
"_TUTORIAL001.PY_COMPLETE": "complete_bash",
"COMP_WORDS": "tutorial001.py del",
"COMP_CWORD": "1",
- "_TYPER_COMPLETE_TESTING": "True",
},
)
assert "delete\ndelete-all" in result.stdout
@@ -31,7 +30,6 @@ def test_completion_complete_subcommand_bash_invalid():
"_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
@@ -46,7 +44,6 @@ def test_completion_complete_subcommand_zsh():
**os.environ,
"_TUTORIAL001.PY_COMPLETE": "complete_zsh",
"_TYPER_COMPLETE_ARGS": "tutorial001.py del",
- "_TYPER_COMPLETE_TESTING": "True",
},
)
assert (
@@ -64,7 +61,6 @@ def test_completion_complete_subcommand_zsh_files():
**os.environ,
"_TUTORIAL001.PY_COMPLETE": "complete_zsh",
"_TYPER_COMPLETE_ARGS": "tutorial001.py delete ",
- "_TYPER_COMPLETE_TESTING": "True",
},
)
assert ("_files") in result.stdout
@@ -80,7 +76,6 @@ def test_completion_complete_subcommand_fish():
"_TUTORIAL001.PY_COMPLETE": "complete_fish",
"_TYPER_COMPLETE_ARGS": "tutorial001.py del",
"_TYPER_COMPLETE_FISH_ACTION": "get-args",
- "_TYPER_COMPLETE_TESTING": "True",
},
)
assert (
@@ -99,7 +94,6 @@ def test_completion_complete_subcommand_fish_should_complete():
"_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
@@ -115,7 +109,6 @@ def test_completion_complete_subcommand_fish_should_complete_no():
"_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
@@ -130,7 +123,6 @@ def test_completion_complete_subcommand_powershell():
**os.environ,
"_TUTORIAL001.PY_COMPLETE": "complete_powershell",
"_TYPER_COMPLETE_ARGS": "tutorial001.py del",
- "_TYPER_COMPLETE_TESTING": "True",
},
)
assert (
@@ -147,7 +139,6 @@ def test_completion_complete_subcommand_pwsh():
**os.environ,
"_TUTORIAL001.PY_COMPLETE": "complete_pwsh",
"_TYPER_COMPLETE_ARGS": "tutorial001.py del",
- "_TYPER_COMPLETE_TESTING": "True",
},
)
assert (
@@ -164,7 +155,6 @@ def test_completion_complete_subcommand_noshell():
**os.environ,
"_TUTORIAL001.PY_COMPLETE": "complete_noshell",
"_TYPER_COMPLETE_ARGS": "tutorial001.py del",
- "_TYPER_COMPLETE_TESTING": "True",
},
)
assert ("") in result.stdout
diff --git a/tests/test_completion/test_completion_complete_no_help.py b/tests/test_completion/test_completion_complete_no_help.py
index c67c77a82b..4ac2bf98de 100644
--- a/tests/test_completion/test_completion_complete_no_help.py
+++ b/tests/test_completion/test_completion_complete_no_help.py
@@ -14,7 +14,6 @@ def test_completion_complete_subcommand_zsh():
**os.environ,
"_TUTORIAL002.PY_COMPLETE": "complete_zsh",
"_TYPER_COMPLETE_ARGS": "tutorial002.py ",
- "_TYPER_COMPLETE_TESTING": "True",
},
)
assert "create" in result.stdout
@@ -31,7 +30,6 @@ def test_completion_complete_subcommand_fish():
"_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
@@ -46,7 +44,6 @@ def test_completion_complete_subcommand_powershell():
**os.environ,
"_TUTORIAL002.PY_COMPLETE": "complete_powershell",
"_TYPER_COMPLETE_ARGS": "tutorial002.py ",
- "_TYPER_COMPLETE_TESTING": "True",
},
)
assert ("create::: \ndelete::: ") in result.stdout
@@ -61,7 +58,6 @@ def test_completion_complete_subcommand_pwsh():
**os.environ,
"_TUTORIAL002.PY_COMPLETE": "complete_pwsh",
"_TYPER_COMPLETE_ARGS": "tutorial002.py ",
- "_TYPER_COMPLETE_TESTING": "True",
},
)
assert ("create::: \ndelete::: ") in result.stdout
diff --git a/tests/test_completion/test_completion_install.py b/tests/test_completion/test_completion_install.py
index 7a3eac4a77..a6ad3c9f03 100644
--- a/tests/test_completion/test_completion_install.py
+++ b/tests/test_completion/test_completion_install.py
@@ -22,7 +22,6 @@ def test_completion_install_no_shell():
encoding="utf-8",
env={
**os.environ,
- "_TYPER_COMPLETE_TESTING": "True",
"_TYPER_COMPLETE_TEST_DISABLE_SHELL_DETECTION": "True",
},
)
@@ -48,7 +47,6 @@ def test_completion_install_bash():
encoding="utf-8",
env={
**os.environ,
- "_TYPER_COMPLETE_TESTING": "True",
"_TYPER_COMPLETE_TEST_DISABLE_SHELL_DETECTION": "True",
},
)
@@ -90,7 +88,6 @@ def test_completion_install_zsh():
encoding="utf-8",
env={
**os.environ,
- "_TYPER_COMPLETE_TESTING": "True",
"_TYPER_COMPLETE_TEST_DISABLE_SHELL_DETECTION": "True",
},
)
@@ -126,7 +123,6 @@ def test_completion_install_fish():
encoding="utf-8",
env={
**os.environ,
- "_TYPER_COMPLETE_TESTING": "True",
"_TYPER_COMPLETE_TEST_DISABLE_SHELL_DETECTION": "True",
},
)
diff --git a/tests/test_completion/test_completion_show.py b/tests/test_completion/test_completion_show.py
index 5f94d50747..af4ed2a90e 100644
--- a/tests/test_completion/test_completion_show.py
+++ b/tests/test_completion/test_completion_show.py
@@ -21,7 +21,6 @@ def test_completion_show_no_shell():
encoding="utf-8",
env={
**os.environ,
- "_TYPER_COMPLETE_TESTING": "True",
"_TYPER_COMPLETE_TEST_DISABLE_SHELL_DETECTION": "True",
},
)
@@ -43,7 +42,6 @@ def test_completion_show_bash():
encoding="utf-8",
env={
**os.environ,
- "_TYPER_COMPLETE_TESTING": "True",
"_TYPER_COMPLETE_TEST_DISABLE_SHELL_DETECTION": "True",
},
)
@@ -68,7 +66,6 @@ def test_completion_source_zsh():
encoding="utf-8",
env={
**os.environ,
- "_TYPER_COMPLETE_TESTING": "True",
"_TYPER_COMPLETE_TEST_DISABLE_SHELL_DETECTION": "True",
},
)
@@ -90,7 +87,6 @@ def test_completion_source_fish():
encoding="utf-8",
env={
**os.environ,
- "_TYPER_COMPLETE_TESTING": "True",
"_TYPER_COMPLETE_TEST_DISABLE_SHELL_DETECTION": "True",
},
)
@@ -112,7 +108,6 @@ def test_completion_source_powershell():
encoding="utf-8",
env={
**os.environ,
- "_TYPER_COMPLETE_TESTING": "True",
"_TYPER_COMPLETE_TEST_DISABLE_SHELL_DETECTION": "True",
},
)
@@ -137,7 +132,6 @@ def test_completion_source_pwsh():
encoding="utf-8",
env={
**os.environ,
- "_TYPER_COMPLETE_TESTING": "True",
"_TYPER_COMPLETE_TEST_DISABLE_SHELL_DETECTION": "True",
},
)
diff --git a/tests/test_others.py b/tests/test_others.py
index 367a8caeba..7986e1d7f0 100644
--- a/tests/test_others.py
+++ b/tests/test_others.py
@@ -151,7 +151,6 @@ def test_completion_untyped_parameters():
**os.environ,
"_COMPLETION_NO_TYPES.PY_COMPLETE": "complete_zsh",
"_TYPER_COMPLETE_ARGS": "completion_no_types.py --name Sebastian --name Ca",
- "_TYPER_COMPLETE_TESTING": "True",
},
)
assert "info name is: completion_no_types.py" in result.stderr
@@ -178,7 +177,6 @@ def test_completion_untyped_parameters_different_order_correct_names():
**os.environ,
"_COMPLETION_NO_TYPES_ORDER.PY_COMPLETE": "complete_zsh",
"_TYPER_COMPLETE_ARGS": "completion_no_types_order.py --name Sebastian --name Ca",
- "_TYPER_COMPLETE_TESTING": "True",
},
)
assert "info name is: completion_no_types_order.py" in result.stderr
diff --git a/tests/test_tutorial/test_options/test_callback/test_tutorial003.py b/tests/test_tutorial/test_options/test_callback/test_tutorial003.py
index fe2ebd9d08..cbdaa8d094 100644
--- a/tests/test_tutorial/test_options/test_callback/test_tutorial003.py
+++ b/tests/test_tutorial/test_options/test_callback/test_tutorial003.py
@@ -45,7 +45,6 @@ def test_completion():
"_TUTORIAL003.PY_COMPLETE": "complete_bash",
"COMP_WORDS": "tutorial003.py --",
"COMP_CWORD": "1",
- "_TYPER_COMPLETE_TESTING": "True",
},
)
assert "--name" in result.stdout
diff --git a/tests/test_tutorial/test_options/test_callback/test_tutorial003_an.py b/tests/test_tutorial/test_options/test_callback/test_tutorial003_an.py
index cb2715f02e..a80dbcd2bd 100644
--- a/tests/test_tutorial/test_options/test_callback/test_tutorial003_an.py
+++ b/tests/test_tutorial/test_options/test_callback/test_tutorial003_an.py
@@ -45,7 +45,6 @@ def test_completion():
"_TUTORIAL003_AN.PY_COMPLETE": "complete_bash",
"COMP_WORDS": "tutorial003_an.py --",
"COMP_CWORD": "1",
- "_TYPER_COMPLETE_TESTING": "True",
},
)
assert "--name" in result.stdout
diff --git a/tests/test_tutorial/test_options/test_callback/test_tutorial004.py b/tests/test_tutorial/test_options/test_callback/test_tutorial004.py
index 68b74448f7..70f9c1d399 100644
--- a/tests/test_tutorial/test_options/test_callback/test_tutorial004.py
+++ b/tests/test_tutorial/test_options/test_callback/test_tutorial004.py
@@ -45,7 +45,6 @@ def test_completion():
"_TUTORIAL004.PY_COMPLETE": "complete_bash",
"COMP_WORDS": "tutorial004.py --",
"COMP_CWORD": "1",
- "_TYPER_COMPLETE_TESTING": "True",
},
)
assert "--name" in result.stdout
diff --git a/tests/test_tutorial/test_options/test_callback/test_tutorial004_an.py b/tests/test_tutorial/test_options/test_callback/test_tutorial004_an.py
index b410fa7044..7756b202d6 100644
--- a/tests/test_tutorial/test_options/test_callback/test_tutorial004_an.py
+++ b/tests/test_tutorial/test_options/test_callback/test_tutorial004_an.py
@@ -45,7 +45,6 @@ def test_completion():
"_TUTORIAL004_AN.PY_COMPLETE": "complete_bash",
"COMP_WORDS": "tutorial004_an.py --",
"COMP_CWORD": "1",
- "_TYPER_COMPLETE_TESTING": "True",
},
)
assert "--name" in result.stdout
diff --git a/tests/test_tutorial/test_options/test_version/test_tutorial003.py b/tests/test_tutorial/test_options/test_version/test_tutorial003.py
index 4287a5db0c..bcede6fc28 100644
--- a/tests/test_tutorial/test_options/test_version/test_tutorial003.py
+++ b/tests/test_tutorial/test_options/test_version/test_tutorial003.py
@@ -50,7 +50,6 @@ def test_completion():
"_TUTORIAL003.PY_COMPLETE": "complete_bash",
"COMP_WORDS": "tutorial003.py --name Rick --v",
"COMP_CWORD": "3",
- "_TYPER_COMPLETE_TESTING": "True",
},
)
assert "--version" in result.stdout
diff --git a/tests/test_tutorial/test_options/test_version/test_tutorial003_an.py b/tests/test_tutorial/test_options/test_version/test_tutorial003_an.py
index 3081cb440f..5ccf2c922a 100644
--- a/tests/test_tutorial/test_options/test_version/test_tutorial003_an.py
+++ b/tests/test_tutorial/test_options/test_version/test_tutorial003_an.py
@@ -50,7 +50,6 @@ def test_completion():
"_TUTORIAL003_AN.PY_COMPLETE": "complete_bash",
"COMP_WORDS": "tutorial003_an.py --name Rick --v",
"COMP_CWORD": "3",
- "_TYPER_COMPLETE_TESTING": "True",
},
)
assert "--version" in result.stdout
diff --git a/tests/test_tutorial/test_options_autocompletion/test_tutorial002.py b/tests/test_tutorial/test_options_autocompletion/test_tutorial002.py
index 49af214900..3e15fad9b6 100644
--- a/tests/test_tutorial/test_options_autocompletion/test_tutorial002.py
+++ b/tests/test_tutorial/test_options_autocompletion/test_tutorial002.py
@@ -18,7 +18,6 @@ def test_completion():
**os.environ,
"_TUTORIAL002.PY_COMPLETE": "complete_zsh",
"_TYPER_COMPLETE_ARGS": "tutorial002.py --name ",
- "_TYPER_COMPLETE_TESTING": "True",
},
)
assert "Camila" in result.stdout
diff --git a/tests/test_tutorial/test_options_autocompletion/test_tutorial002_an.py b/tests/test_tutorial/test_options_autocompletion/test_tutorial002_an.py
index 94f32fb21a..7db64bfa44 100644
--- a/tests/test_tutorial/test_options_autocompletion/test_tutorial002_an.py
+++ b/tests/test_tutorial/test_options_autocompletion/test_tutorial002_an.py
@@ -18,7 +18,6 @@ def test_completion():
**os.environ,
"_TUTORIAL002_AN.PY_COMPLETE": "complete_zsh",
"_TYPER_COMPLETE_ARGS": "tutorial002_an.py --name ",
- "_TYPER_COMPLETE_TESTING": "True",
},
)
assert "Camila" in result.stdout
diff --git a/tests/test_tutorial/test_options_autocompletion/test_tutorial003.py b/tests/test_tutorial/test_options_autocompletion/test_tutorial003.py
index 5870e445c3..ebd1e066ba 100644
--- a/tests/test_tutorial/test_options_autocompletion/test_tutorial003.py
+++ b/tests/test_tutorial/test_options_autocompletion/test_tutorial003.py
@@ -18,7 +18,6 @@ def test_completion():
**os.environ,
"_TUTORIAL003.PY_COMPLETE": "complete_zsh",
"_TYPER_COMPLETE_ARGS": "tutorial003.py --name Seb",
- "_TYPER_COMPLETE_TESTING": "True",
},
)
assert "Camila" not in result.stdout
diff --git a/tests/test_tutorial/test_options_autocompletion/test_tutorial003_an.py b/tests/test_tutorial/test_options_autocompletion/test_tutorial003_an.py
index 6583f9dccf..8f12583e80 100644
--- a/tests/test_tutorial/test_options_autocompletion/test_tutorial003_an.py
+++ b/tests/test_tutorial/test_options_autocompletion/test_tutorial003_an.py
@@ -18,7 +18,6 @@ def test_completion():
**os.environ,
"_TUTORIAL003_AN.PY_COMPLETE": "complete_zsh",
"_TYPER_COMPLETE_ARGS": "tutorial003_an.py --name Seb",
- "_TYPER_COMPLETE_TESTING": "True",
},
)
assert "Camila" not in result.stdout
diff --git a/tests/test_tutorial/test_options_autocompletion/test_tutorial004.py b/tests/test_tutorial/test_options_autocompletion/test_tutorial004.py
index 1d7784ad2b..17c5f5197a 100644
--- a/tests/test_tutorial/test_options_autocompletion/test_tutorial004.py
+++ b/tests/test_tutorial/test_options_autocompletion/test_tutorial004.py
@@ -18,7 +18,6 @@ def test_completion():
**os.environ,
"_TUTORIAL004.PY_COMPLETE": "complete_zsh",
"_TYPER_COMPLETE_ARGS": "tutorial004_aux.py --name ",
- "_TYPER_COMPLETE_TESTING": "True",
},
)
assert '"Camila":"The reader of books."' in result.stdout
diff --git a/tests/test_tutorial/test_options_autocompletion/test_tutorial004_an.py b/tests/test_tutorial/test_options_autocompletion/test_tutorial004_an.py
index 30196732c6..dcfce0a4a7 100644
--- a/tests/test_tutorial/test_options_autocompletion/test_tutorial004_an.py
+++ b/tests/test_tutorial/test_options_autocompletion/test_tutorial004_an.py
@@ -18,7 +18,6 @@ def test_completion():
**os.environ,
"_TUTORIAL004_AN.PY_COMPLETE": "complete_zsh",
"_TYPER_COMPLETE_ARGS": "tutorial004_an_aux.py --name ",
- "_TYPER_COMPLETE_TESTING": "True",
},
)
assert '"Camila":"The reader of books."' in result.stdout
diff --git a/tests/test_tutorial/test_options_autocompletion/test_tutorial007.py b/tests/test_tutorial/test_options_autocompletion/test_tutorial007.py
index bfa2aedb8a..b8ce1b1598 100644
--- a/tests/test_tutorial/test_options_autocompletion/test_tutorial007.py
+++ b/tests/test_tutorial/test_options_autocompletion/test_tutorial007.py
@@ -18,7 +18,6 @@ def test_completion():
**os.environ,
"_TUTORIAL007.PY_COMPLETE": "complete_zsh",
"_TYPER_COMPLETE_ARGS": "tutorial007.py --name Sebastian --name ",
- "_TYPER_COMPLETE_TESTING": "True",
},
)
assert '"Camila":"The reader of books."' in result.stdout
diff --git a/tests/test_tutorial/test_options_autocompletion/test_tutorial007_an.py b/tests/test_tutorial/test_options_autocompletion/test_tutorial007_an.py
index 6c8c294886..c35f23eb61 100644
--- a/tests/test_tutorial/test_options_autocompletion/test_tutorial007_an.py
+++ b/tests/test_tutorial/test_options_autocompletion/test_tutorial007_an.py
@@ -18,7 +18,6 @@ def test_completion():
**os.environ,
"_TUTORIAL007_AN.PY_COMPLETE": "complete_zsh",
"_TYPER_COMPLETE_ARGS": "tutorial007_an.py --name Sebastian --name ",
- "_TYPER_COMPLETE_TESTING": "True",
},
)
assert '"Camila":"The reader of books."' in result.stdout
diff --git a/tests/test_tutorial/test_options_autocompletion/test_tutorial008.py b/tests/test_tutorial/test_options_autocompletion/test_tutorial008.py
index a347237e71..0874f23c5d 100644
--- a/tests/test_tutorial/test_options_autocompletion/test_tutorial008.py
+++ b/tests/test_tutorial/test_options_autocompletion/test_tutorial008.py
@@ -18,7 +18,6 @@ def test_completion():
**os.environ,
"_TUTORIAL008.PY_COMPLETE": "complete_zsh",
"_TYPER_COMPLETE_ARGS": "tutorial008.py --name ",
- "_TYPER_COMPLETE_TESTING": "True",
},
)
assert '"Camila":"The reader of books."' in result.stdout
diff --git a/tests/test_tutorial/test_options_autocompletion/test_tutorial008_an.py b/tests/test_tutorial/test_options_autocompletion/test_tutorial008_an.py
index ff5c25987c..cb2481a67c 100644
--- a/tests/test_tutorial/test_options_autocompletion/test_tutorial008_an.py
+++ b/tests/test_tutorial/test_options_autocompletion/test_tutorial008_an.py
@@ -18,7 +18,6 @@ def test_completion():
**os.environ,
"_TUTORIAL008_AN.PY_COMPLETE": "complete_zsh",
"_TYPER_COMPLETE_ARGS": "tutorial008_an.py --name ",
- "_TYPER_COMPLETE_TESTING": "True",
},
)
assert '"Camila":"The reader of books."' in result.stdout
diff --git a/tests/test_tutorial/test_options_autocompletion/test_tutorial009.py b/tests/test_tutorial/test_options_autocompletion/test_tutorial009.py
index 8d9a73e755..3c7eb0cc64 100644
--- a/tests/test_tutorial/test_options_autocompletion/test_tutorial009.py
+++ b/tests/test_tutorial/test_options_autocompletion/test_tutorial009.py
@@ -18,7 +18,6 @@ def test_completion():
**os.environ,
"_TUTORIAL009.PY_COMPLETE": "complete_zsh",
"_TYPER_COMPLETE_ARGS": "tutorial009.py --name Sebastian --name ",
- "_TYPER_COMPLETE_TESTING": "True",
},
)
assert '"Camila":"The reader of books."' in result.stdout
diff --git a/tests/test_tutorial/test_options_autocompletion/test_tutorial009_an.py b/tests/test_tutorial/test_options_autocompletion/test_tutorial009_an.py
index fd4b65f1f1..56182ac3b9 100644
--- a/tests/test_tutorial/test_options_autocompletion/test_tutorial009_an.py
+++ b/tests/test_tutorial/test_options_autocompletion/test_tutorial009_an.py
@@ -18,7 +18,6 @@ def test_completion():
**os.environ,
"_TUTORIAL009_AN.PY_COMPLETE": "complete_zsh",
"_TYPER_COMPLETE_ARGS": "tutorial009_an.py --name Sebastian --name ",
- "_TYPER_COMPLETE_TESTING": "True",
},
)
assert '"Camila":"The reader of books."' in result.stdout
diff --git a/typer/__init__.py b/typer/__init__.py
index a659d767b6..0d3a4bbfa9 100644
--- a/typer/__init__.py
+++ b/typer/__init__.py
@@ -1,6 +1,6 @@
"""Typer, build great CLIs. Easy to code. Based on Python type hints."""
-__version__ = "0.11.1"
+__version__ = "0.12.dev2"
from shutil import get_terminal_size as get_terminal_size
diff --git a/typer/__main__.py b/typer/__main__.py
new file mode 100644
index 0000000000..4e28416e10
--- /dev/null
+++ b/typer/__main__.py
@@ -0,0 +1,3 @@
+from .cli import main
+
+main()
diff --git a/typer/cli.py b/typer/cli.py
new file mode 100644
index 0000000000..12a755de48
--- /dev/null
+++ b/typer/cli.py
@@ -0,0 +1,292 @@
+import importlib.util
+import re
+import sys
+from pathlib import Path
+from typing import Any, List, Optional
+
+import click
+import typer
+import typer.core
+from click import Command, Group, Option
+
+from . import __version__
+
+default_app_names = ("app", "cli", "main")
+default_func_names = ("main", "cli", "app")
+
+app = typer.Typer()
+utils_app = typer.Typer(help="Extra utility commands for Typer apps.")
+app.add_typer(utils_app, name="utils")
+
+
+class State:
+ def __init__(self) -> None:
+ self.app: Optional[str] = None
+ self.func: Optional[str] = None
+ self.file: Optional[Path] = None
+ self.module: Optional[str] = None
+
+
+state = State()
+
+
+def maybe_update_state(ctx: click.Context) -> None:
+ path_or_module = ctx.params.get("path_or_module")
+ if path_or_module:
+ file_path = Path(path_or_module)
+ if file_path.exists() and file_path.is_file():
+ state.file = file_path
+ else:
+ if not re.fullmatch(r"[a-zA-Z_]\w*(\.[a-zA-Z_]\w*)*", path_or_module):
+ typer.echo(
+ f"Not a valid file or Python module: {path_or_module}", err=True
+ )
+ sys.exit(1)
+ state.module = path_or_module
+ app_name = ctx.params.get("app")
+ if app_name:
+ state.app = app_name
+ func_name = ctx.params.get("func")
+ if func_name:
+ state.func = func_name
+
+
+class TyperCLIGroup(typer.core.TyperGroup):
+ def list_commands(self, ctx: click.Context) -> List[str]:
+ self.maybe_add_run(ctx)
+ return super().list_commands(ctx)
+
+ def get_command(self, ctx: click.Context, name: str) -> Optional[Command]:
+ self.maybe_add_run(ctx)
+ return super().get_command(ctx, name)
+
+ def invoke(self, ctx: click.Context) -> Any:
+ self.maybe_add_run(ctx)
+ return super().invoke(ctx)
+
+ def maybe_add_run(self, ctx: click.Context) -> None:
+ maybe_update_state(ctx)
+ maybe_add_run_to_cli(self)
+
+
+def get_typer_from_module(module: Any) -> Optional[typer.Typer]:
+ # Try to get defined app
+ if state.app:
+ obj = getattr(module, state.app, None)
+ if not isinstance(obj, typer.Typer):
+ typer.echo(f"Not a Typer object: --app {state.app}", err=True)
+ sys.exit(1)
+ return obj
+ # Try to get defined function
+ if state.func:
+ func_obj = getattr(module, state.func, None)
+ if not callable(func_obj):
+ typer.echo(f"Not a function: --func {state.func}", err=True)
+ sys.exit(1)
+ sub_app = typer.Typer()
+ sub_app.command()(func_obj)
+ return sub_app
+ # Iterate and get a default object to use as CLI
+ local_names = dir(module)
+ local_names_set = set(local_names)
+ # Try to get a default Typer app
+ for name in default_app_names:
+ if name in local_names_set:
+ obj = getattr(module, name, None)
+ if isinstance(obj, typer.Typer):
+ return obj
+ # Try to get any Typer app
+ for name in local_names_set - set(default_app_names):
+ obj = getattr(module, name)
+ if isinstance(obj, typer.Typer):
+ return obj
+ # Try to get a default function
+ for func_name in default_func_names:
+ func_obj = getattr(module, func_name, None)
+ if callable(func_obj):
+ sub_app = typer.Typer()
+ sub_app.command()(func_obj)
+ return sub_app
+ # Try to get any func app
+ for func_name in local_names_set - set(default_func_names):
+ func_obj = getattr(module, func_name)
+ if callable(func_obj):
+ sub_app = typer.Typer()
+ sub_app.command()(func_obj)
+ return sub_app
+ return None
+
+
+def get_typer_from_state() -> Optional[typer.Typer]:
+ spec = None
+ if state.file:
+ module_name = state.file.name
+ spec = importlib.util.spec_from_file_location(module_name, str(state.file))
+ elif state.module:
+ spec = importlib.util.find_spec(state.module)
+ if spec is None:
+ if state.file:
+ typer.echo(f"Could not import as Python file: {state.file}", err=True)
+ else:
+ typer.echo(f"Could not import as Python module: {state.module}", err=True)
+ sys.exit(1)
+ module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(module) # type: ignore
+ obj = get_typer_from_module(module)
+ return obj
+
+
+def maybe_add_run_to_cli(cli: click.Group) -> None:
+ if "run" not in cli.commands:
+ if state.file or state.module:
+ obj = get_typer_from_state()
+ if obj:
+ obj._add_completion = False
+ click_obj = typer.main.get_command(obj)
+ click_obj.name = "run"
+ if not click_obj.help:
+ click_obj.help = "Run the provided Typer app."
+ cli.add_command(click_obj)
+
+
+def print_version(ctx: click.Context, param: Option, value: bool) -> None:
+ if not value or ctx.resilient_parsing:
+ return
+ typer.echo(f"Typer version: {__version__}")
+ raise typer.Exit()
+
+
+@app.callback(cls=TyperCLIGroup, no_args_is_help=True)
+def callback(
+ ctx: typer.Context,
+ *,
+ path_or_module: str = typer.Argument(None),
+ app: str = typer.Option(None, help="The typer app object/variable to use."),
+ func: str = typer.Option(None, help="The function to convert to Typer."),
+ version: bool = typer.Option(
+ False,
+ "--version",
+ help="Print version and exit.",
+ callback=print_version,
+ ),
+) -> None:
+ """
+ Run Typer scripts with completion, without having to create a package.
+
+ You probably want to install completion for the typer command:
+
+ $ typer --install-completion
+
+ https://typer.tiangolo.com/
+ """
+ maybe_update_state(ctx)
+
+
+def get_docs_for_click(
+ *,
+ obj: Command,
+ ctx: typer.Context,
+ indent: int = 0,
+ name: str = "",
+ call_prefix: str = "",
+) -> str:
+ docs = "#" * (1 + indent)
+ command_name = name or obj.name
+ if call_prefix:
+ command_name = f"{call_prefix} {command_name}"
+ title = f"`{command_name}`" if command_name else "CLI"
+ docs += f" {title}\n\n"
+ if obj.help:
+ docs += f"{obj.help}\n\n"
+ usage_pieces = obj.collect_usage_pieces(ctx)
+ if usage_pieces:
+ docs += "**Usage**:\n\n"
+ docs += "```console\n"
+ docs += "$ "
+ if command_name:
+ docs += f"{command_name} "
+ docs += f"{' '.join(usage_pieces)}\n"
+ docs += "```\n\n"
+ args = []
+ opts = []
+ for param in obj.get_params(ctx):
+ rv = param.get_help_record(ctx)
+ if rv is not None:
+ if param.param_type_name == "argument":
+ args.append(rv)
+ elif param.param_type_name == "option":
+ opts.append(rv)
+ if args:
+ docs += "**Arguments**:\n\n"
+ for arg_name, arg_help in args:
+ docs += f"* `{arg_name}`"
+ if arg_help:
+ docs += f": {arg_help}"
+ docs += "\n"
+ docs += "\n"
+ if opts:
+ docs += "**Options**:\n\n"
+ for opt_name, opt_help in opts:
+ docs += f"* `{opt_name}`"
+ if opt_help:
+ docs += f": {opt_help}"
+ docs += "\n"
+ docs += "\n"
+ if obj.epilog:
+ docs += f"{obj.epilog}\n\n"
+ if isinstance(obj, Group):
+ group = obj
+ commands = group.list_commands(ctx)
+ if commands:
+ docs += "**Commands**:\n\n"
+ for command in commands:
+ command_obj = group.get_command(ctx, command)
+ assert command_obj
+ docs += f"* `{command_obj.name}`"
+ command_help = command_obj.get_short_help_str()
+ if command_help:
+ docs += f": {command_help}"
+ docs += "\n"
+ docs += "\n"
+ for command in commands:
+ command_obj = group.get_command(ctx, command)
+ assert command_obj
+ use_prefix = ""
+ if command_name:
+ use_prefix += f"{command_name}"
+ docs += get_docs_for_click(
+ obj=command_obj, ctx=ctx, indent=indent + 1, call_prefix=use_prefix
+ )
+ return docs
+
+
+@utils_app.command()
+def docs(
+ ctx: typer.Context,
+ name: str = typer.Option("", help="The name of the CLI program to use in docs."),
+ output: Path = typer.Option(
+ None,
+ help="An output file to write docs to, like README.md.",
+ file_okay=True,
+ dir_okay=False,
+ ),
+) -> None:
+ """
+ Generate Markdown docs for a Typer app.
+ """
+ typer_obj = get_typer_from_state()
+ if not typer_obj:
+ typer.echo("No Typer app found", err=True)
+ raise typer.Abort()
+ click_obj = typer.main.get_command(typer_obj)
+ docs = get_docs_for_click(obj=click_obj, ctx=ctx, name=name)
+ clean_docs = f"{docs.strip()}\n"
+ if output:
+ output.write_text(clean_docs)
+ typer.echo(f"Docs saved to: {output}")
+ else:
+ typer.echo(clean_docs)
+
+
+def main() -> Any:
+ return app()
diff --git a/typer_cli_package/README.md b/typer_cli_package/README.md
new file mode 100644
index 0000000000..dba1a0dcf7
--- /dev/null
+++ b/typer_cli_package/README.md
@@ -0,0 +1,49 @@
+
+
+
+
+ Typer, build great CLIs. Easy to code. Based on Python type hints.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+---
+
+**Documentation**:
https://typer.tiangolo.com/tutorial/typer-command/
+
+**Source Code**:
https://github.com/tiangolo/typer
+
+---
+
+Typer is a library for building
CLI applications that users will **love using** and developers will **love creating**. Based on Python type hints.
+
+## Typer CLI
+
+This package, `typer-cli`, only provides a command `typer` in the shell with the same functionality of `python -m typer`.
+
+The only reason why this is a separate package is to allow developers to opt out of the `typer` command by installing `typer-slim`, that doesn't include `typer-cli`.
+
+You probably **should not** install this package.
+
+Install instead:
+
+```bash
+pip install typer
+```
+
+That includes this package, `typer-cli`, with the `typer` command.
+
+## License
+
+This project is licensed under the terms of the MIT license.
diff --git a/typer_cli_package/pdm_build.py b/typer_cli_package/pdm_build.py
new file mode 100644
index 0000000000..cca6d59a75
--- /dev/null
+++ b/typer_cli_package/pdm_build.py
@@ -0,0 +1,40 @@
+from pathlib import Path
+from typing import List
+
+from pdm.backend.hooks import Context
+
+packages_to_sync = ["typer-slim"]
+
+license_name = "LICENSE"
+license_path = Path("..") / license_name
+
+
+def pdm_build_hook_enabled(context: Context):
+ return context.target == "sdist"
+
+
+def pdm_build_initialize(context: Context):
+ metadata = context.config.metadata
+ # Get main version
+ version = metadata["version"]
+ # Update version in dependencies to sync them
+ dependencies: List[str] = metadata["dependencies"]
+ new_dependencies = []
+ for dep in dependencies:
+ if any(dep.startswith(name) for name in packages_to_sync):
+ new_dep = f"{dep}=={version}"
+ new_dependencies.append(new_dep)
+ else:
+ new_dependencies.append(dep)
+ if new_dependencies != dependencies:
+ metadata["dependencies"] = new_dependencies
+ # LICENSE
+ license_content = license_path.read_text()
+ context.ensure_build_dir()
+ # # Workaround, copy LICENSE to package_dir during build
+ Path(license_name).write_text(license_content)
+
+
+def pdm_build_finalize(context: Context, artifact: Path):
+ # Workaround, remove LICENSE from package_dir after build
+ Path(license_name).unlink()
diff --git a/typer_cli_package/pyproject.toml b/typer_cli_package/pyproject.toml
new file mode 100644
index 0000000000..2bb4f8d26c
--- /dev/null
+++ b/typer_cli_package/pyproject.toml
@@ -0,0 +1,55 @@
+[build-system]
+requires = ["pdm-backend"]
+build-backend = "pdm.backend"
+
+[project]
+name = "typer-cli"
+dynamic = ["version"]
+description = "Typer, build great CLIs. Easy to code. Based on Python type hints."
+authors = [
+ {name = "Sebastián Ramírez", email = "tiangolo@gmail.com"},
+]
+readme = "README.md"
+requires-python = ">=3.7"
+classifiers = [
+ "Intended Audience :: Information Technology",
+ "Intended Audience :: System Administrators",
+ "Operating System :: OS Independent",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python",
+ "Topic :: Software Development :: Libraries :: Application Frameworks",
+ "Topic :: Software Development :: Libraries :: Python Modules",
+ "Topic :: Software Development :: Libraries",
+ "Topic :: Software Development",
+ "Typing :: Typed",
+ "Development Status :: 4 - Beta",
+ "Intended Audience :: Developers",
+ "Programming Language :: Python :: 3 :: Only",
+ "Programming Language :: Python :: 3.7",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "License :: OSI Approved :: MIT License",
+]
+dependencies = [
+ "typer-slim[standard]",
+]
+
+[project.urls]
+Documentation = "https://typer.tiangolo.com/"
+homepage = "https://github.com/tiangolo/typer"
+
+[project.optional-dependencies]
+# For backwards compatibility
+all = []
+
+[project.scripts]
+typer = "typer.cli:main"
+
+[tool.pdm]
+version = { source = "file", path = "../typer/__init__.py" }
+distribution = true
+
+[tool.pdm.build]
+excludes = ["*"]
diff --git a/typer_package/pdm_build.py b/typer_package/pdm_build.py
new file mode 100644
index 0000000000..b670aa9504
--- /dev/null
+++ b/typer_package/pdm_build.py
@@ -0,0 +1,49 @@
+from pathlib import Path
+from typing import List
+
+from pdm.backend.hooks import Context
+
+packages_to_sync = ["typer-slim", "typer-cli"]
+
+readme_name = "README.md"
+license_name = "LICENSE"
+readme_path = Path("..") / readme_name
+license_path = Path("..") / license_name
+
+
+def pdm_build_hook_enabled(context: Context):
+ return context.target == "sdist"
+
+
+def pdm_build_initialize(context: Context):
+ metadata = context.config.metadata
+ # Get main version
+ version = metadata["version"]
+ # Update version in dependencies to sync them
+ dependencies: List[str] = metadata["dependencies"]
+ new_dependencies = []
+ for dep in dependencies:
+ if any(dep.startswith(name) for name in packages_to_sync):
+ new_dep = f"{dep}=={version}"
+ new_dependencies.append(new_dep)
+ else:
+ new_dependencies.append(dep)
+ if new_dependencies != dependencies:
+ metadata["dependencies"] = new_dependencies
+ # README.md and LICENSE
+ readme_content = readme_path.read_text()
+ license_content = license_path.read_text()
+ metadata["readme"] = readme_name
+ context.ensure_build_dir()
+ # Copy README.md and LICENSE to build directory
+ # (context.build_dir / readme_name).write_text(readme_content)
+ # (context.build_dir / license_name).write_text(license_content)
+ # # Workaround, copy README.md and LICENSE to package_dir during build
+ Path(readme_name).write_text(readme_content)
+ Path(license_name).write_text(license_content)
+
+
+def pdm_build_finalize(context: Context, artifact: Path):
+ # Workaround, remove README.md and LICENSE from package_dir after build
+ Path(readme_name).unlink()
+ Path(license_name).unlink()
diff --git a/typer_package/pyproject.toml b/typer_package/pyproject.toml
new file mode 100644
index 0000000000..c2f3f45bfa
--- /dev/null
+++ b/typer_package/pyproject.toml
@@ -0,0 +1,52 @@
+[build-system]
+requires = ["pdm-backend"]
+build-backend = "pdm.backend"
+
+[project]
+name = "typer"
+dynamic = ["version"]
+description = "Typer, build great CLIs. Easy to code. Based on Python type hints."
+authors = [
+ {name = "Sebastián Ramírez", email = "tiangolo@gmail.com"},
+]
+requires-python = ">=3.7"
+classifiers = [
+ "Intended Audience :: Information Technology",
+ "Intended Audience :: System Administrators",
+ "Operating System :: OS Independent",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python",
+ "Topic :: Software Development :: Libraries :: Application Frameworks",
+ "Topic :: Software Development :: Libraries :: Python Modules",
+ "Topic :: Software Development :: Libraries",
+ "Topic :: Software Development",
+ "Typing :: Typed",
+ "Development Status :: 4 - Beta",
+ "Intended Audience :: Developers",
+ "Programming Language :: Python :: 3 :: Only",
+ "Programming Language :: Python :: 3.7",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "License :: OSI Approved :: MIT License",
+]
+dependencies = [
+ "typer-slim[standard]",
+ "typer-cli",
+]
+
+[project.urls]
+Documentation = "https://typer.tiangolo.com/"
+homepage = "https://github.com/tiangolo/typer"
+
+[project.optional-dependencies]
+# For backwards compatibility
+all = []
+
+[tool.pdm]
+version = { source = "file", path = "../typer/__init__.py" }
+distribution = true
+
+[tool.pdm.build]
+excludes = ["*"]