From 7d5d6bb9e6eed6ef53271e78727b467f362504c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 29 Mar 2024 21:09:05 -0500 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20`typer-slim`=20package=20with?= =?UTF-8?q?out=20extras,=20make=20`typer`=20include=20`typer-slim[default]?= =?UTF-8?q?`=20and=20integrate=20Typer=20CLI=20(`typer`=20command)=20into?= =?UTF-8?q?=20Typer=20(#780)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/publish.yml | 17 +- .github/workflows/test-redistribute.yml | 23 +- README.md | 117 +++++-- docs/features.md | 8 +- docs/index.md | 117 +++++-- docs/tutorial/first-steps.md | 10 +- docs/tutorial/index.md | 23 +- docs/tutorial/options-autocompletion.md | 8 +- docs/tutorial/options/callback-and-context.md | 4 +- docs/tutorial/package.md | 11 +- docs/tutorial/printing.md | 22 +- .../typer-command.md} | 128 +++----- mkdocs.insiders.yml | 3 +- mkdocs.yml | 7 +- pyproject.toml | 13 +- tests/assets/cli/__init__.py | 0 tests/assets/cli/app_other_name.py | 8 + tests/assets/cli/empty_script.py | 0 tests/assets/cli/func_other_name.py | 2 + tests/assets/cli/multi_app.py | 40 +++ tests/assets/cli/multi_app_cli.py | 22 ++ tests/assets/cli/multi_func.py | 12 + tests/assets/cli/multiapp-docs.md | 102 ++++++ tests/assets/cli/not_python.txt | 1 + tests/assets/cli/sample.py | 25 ++ tests/test_cli/__init__.py | 0 tests/test_cli/test_app_other_name.py | 41 +++ tests/test_cli/test_completion_run.py | 19 ++ tests/test_cli/test_doc.py | 112 +++++++ tests/test_cli/test_empty_script.py | 20 ++ tests/test_cli/test_func_other_name.py | 22 ++ tests/test_cli/test_help.py | 38 +++ tests/test_cli/test_multi_app.py | 123 ++++++++ tests/test_cli/test_multi_app_cli.py | 102 ++++++ tests/test_cli/test_multi_app_sub.py | 46 +++ tests/test_cli/test_multi_func.py | 106 +++++++ tests/test_cli/test_not_python.py | 20 ++ tests/test_cli/test_sub.py | 139 +++++++++ tests/test_cli/test_sub_completion.py | 19 ++ tests/test_cli/test_sub_help.py | 24 ++ tests/test_cli/test_version.py | 11 + tests/test_compat/test_option_get_help.py | 1 - tests/test_completion/test_completion.py | 8 - .../test_completion_complete.py | 10 - .../test_completion_complete_no_help.py | 4 - .../test_completion_install.py | 4 - tests/test_completion/test_completion_show.py | 6 - tests/test_others.py | 2 - .../test_callback/test_tutorial003.py | 1 - .../test_callback/test_tutorial003_an.py | 1 - .../test_callback/test_tutorial004.py | 1 - .../test_callback/test_tutorial004_an.py | 1 - .../test_version/test_tutorial003.py | 1 - .../test_version/test_tutorial003_an.py | 1 - .../test_tutorial002.py | 1 - .../test_tutorial002_an.py | 1 - .../test_tutorial003.py | 1 - .../test_tutorial003_an.py | 1 - .../test_tutorial004.py | 1 - .../test_tutorial004_an.py | 1 - .../test_tutorial007.py | 1 - .../test_tutorial007_an.py | 1 - .../test_tutorial008.py | 1 - .../test_tutorial008_an.py | 1 - .../test_tutorial009.py | 1 - .../test_tutorial009_an.py | 1 - typer/__init__.py | 2 +- typer/__main__.py | 3 + typer/cli.py | 292 ++++++++++++++++++ typer_cli_package/README.md | 49 +++ typer_cli_package/pdm_build.py | 40 +++ typer_cli_package/pyproject.toml | 55 ++++ typer_package/pdm_build.py | 49 +++ typer_package/pyproject.toml | 52 ++++ 74 files changed, 1900 insertions(+), 260 deletions(-) rename docs/{typer-cli.md => tutorial/typer-command.md} (61%) create mode 100644 tests/assets/cli/__init__.py create mode 100644 tests/assets/cli/app_other_name.py create mode 100644 tests/assets/cli/empty_script.py create mode 100644 tests/assets/cli/func_other_name.py create mode 100644 tests/assets/cli/multi_app.py create mode 100644 tests/assets/cli/multi_app_cli.py create mode 100644 tests/assets/cli/multi_func.py create mode 100644 tests/assets/cli/multiapp-docs.md create mode 100644 tests/assets/cli/not_python.txt create mode 100644 tests/assets/cli/sample.py create mode 100644 tests/test_cli/__init__.py create mode 100644 tests/test_cli/test_app_other_name.py create mode 100644 tests/test_cli/test_completion_run.py create mode 100644 tests/test_cli/test_doc.py create mode 100644 tests/test_cli/test_empty_script.py create mode 100644 tests/test_cli/test_func_other_name.py create mode 100644 tests/test_cli/test_help.py create mode 100644 tests/test_cli/test_multi_app.py create mode 100644 tests/test_cli/test_multi_app_cli.py create mode 100644 tests/test_cli/test_multi_app_sub.py create mode 100644 tests/test_cli/test_multi_func.py create mode 100644 tests/test_cli/test_not_python.py create mode 100644 tests/test_cli/test_sub.py create mode 100644 tests/test_cli/test_sub_completion.py create mode 100644 tests/test_cli/test_sub_help.py create mode 100644 tests/test_cli/test_version.py create mode 100644 typer/__main__.py create mode 100644 typer/cli.py create mode 100644 typer_cli_package/README.md create mode 100644 typer_cli_package/pdm_build.py create mode 100644 typer_cli_package/pyproject.toml create mode 100644 typer_package/pdm_build.py create mode 100644 typer_package/pyproject.toml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 899e49057f..1a842c4ea7 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -8,6 +8,16 @@ on: jobs: publish: runs-on: ubuntu-latest + strategy: + matrix: + package: + - typer-slim + - typer + - typer-cli + env: + dir: ${{ matrix.package == 'typer-slim' && './' || matrix.package == 'typer' && 'typer_package' || matrix.package == 'typer-cli' && 'typer_cli_package' }} + permissions: + id-token: write steps: - name: Dump GitHub context env: @@ -24,12 +34,9 @@ jobs: - name: Install build dependencies run: pip install build - name: Build distribution + working-directory: ${{ env.dir }} run: python -m build - name: Publish uses: pypa/gh-action-pypi-publish@v1.8.11 with: - password: ${{ secrets.PYPI_API_TOKEN }} - - name: Dump GitHub context - env: - GITHUB_CONTEXT: ${{ toJson(github) }} - run: echo "$GITHUB_CONTEXT" + packages-dir: ${{ env.dir }}/dist/ diff --git a/.github/workflows/test-redistribute.yml b/.github/workflows/test-redistribute.yml index 9ef1cde73c..c16d19d7b4 100644 --- a/.github/workflows/test-redistribute.yml +++ b/.github/workflows/test-redistribute.yml @@ -12,6 +12,14 @@ on: jobs: test-redistribute: runs-on: ubuntu-latest + strategy: + matrix: + package: + - typer-slim + - typer + - typer-cli + env: + dir: ${{ matrix.package == 'typer-slim' && './' || matrix.package == 'typer' && 'typer_package' || matrix.package == 'typer-cli' && 'typer_cli_package' }} steps: - name: Dump GitHub context env: @@ -28,24 +36,25 @@ jobs: - name: Install build dependencies run: pip install build - name: Build source distribution + working-directory: ${{ env.dir }} run: python -m build --sdist - name: Decompress source distribution + working-directory: ${{ env.dir }} run: | cd dist tar xvf typer*.tar.gz - name: Install test dependencies + if: ${{ matrix.package == 'typer-slim' }} run: | - cd dist/typer-*/ + cd dist/typer*/ pip install -r requirements-tests.txt - name: Run source distribution tests + if: ${{ matrix.package == 'typer-slim' }} run: | - cd dist/typer-*/ + cd dist/typer*/ bash scripts/test.sh - name: Build wheel distribution + working-directory: ${{ env.dir }} run: | cd dist - pip wheel --no-deps typer-*.tar.gz - - name: Dump GitHub context - env: - GITHUB_CONTEXT: ${{ toJson(github) }} - run: echo "$GITHUB_CONTEXT" + pip wheel --no-deps typer*.tar.gz diff --git a/README.md b/README.md index 303774709f..0d4d824aa1 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,8 @@ Typer is a library for building CLI applications that users will **love using** and developers will **love creating**. Based on Python type hints. +It's also a command line tool to run scripts, automatically converting them to CLI applications. + The key features are: * **Intuitive to write**: Great editor support. Completion everywhere. Less time debugging. Designed to be easy to use and learn. Less time reading docs. @@ -35,39 +37,87 @@ The key features are: * **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs. * **Start simple**: The simplest example adds only 2 lines of code to your app: **1 import, 1 function call**. * **Grow large**: Grow in complexity as much as you want, create arbitrarily complex trees of commands and groups of subcommands, with options and arguments. +* **Run scripts**: Typer includes a `typer` command that you can use to run scripts, automatically converting them to CLIs, even if they don't use Typer internally. ## FastAPI of CLIs - - -**Typer** is FastAPI's little sibling. - -And it's intended to be the FastAPI of CLIs. - -## Requirements - -**Typer** stands on the shoulders of a giant. Its only internal dependency is Click. +**Typer** is FastAPI's little sibling, it's the FastAPI of CLIs. ## Installation
```console -$ pip install "typer[all]" +$ pip install typer ---> 100% -Successfully installed typer +Successfully installed typer rich shellingham ```
-**Note**: that will include Rich. Rich is the recommended library to *display* information on the terminal, it is optional, but when installed, it's deeply integrated into **Typer** to display beautiful output. - ## Example ### The absolute minimum * Create a file `main.py` with: +```Python +def main(name: str): + print(f"Hello {name}") +``` + +This script doesn't even use Typer internally. But you can use the `typer` command to run it as a CLI application. + +### Run it + +Run your application with the `typer` command: + +
+ +```console +// Run your application +$ typer main.py run + +// You get a nice error, you are missing NAME +Usage: typer [PATH_OR_MODULE] run [OPTIONS] NAME +Try 'typer [PATH_OR_MODULE] run --help' for help. +╭─ Error ───────────────────────────────────────────╮ +│ Missing argument 'NAME'. │ +╰───────────────────────────────────────────────────╯ + + +// You get a --help for free +$ typer main.py run --help + +Usage: typer [PATH_OR_MODULE] run [OPTIONS] NAME + +Run the provided Typer app. + +╭─ Arguments ───────────────────────────────────────╮ +│ * name TEXT [default: None] [required] | +╰───────────────────────────────────────────────────╯ +╭─ Options ─────────────────────────────────────────╮ +│ --help Show this message and exit. │ +╰───────────────────────────────────────────────────╯ + +// Now pass the NAME argument +$ typer main.py run Camila + +Hello Camila + +// It works! 🎉 +``` + +
+ +This is the simplest use case, not even using Typer internally, but it can already be quite useful for simple scripts. + +**Note**: auto-completion works when you create a Python package and run it with `--install-completion` or when you use the `typer` command. + +## Use Typer in your code + +Now let's start using Typer in your own code, update `main.py` with: + ```Python import typer @@ -80,9 +130,7 @@ if __name__ == "__main__": typer.run(main) ``` -### Run it - -Run your application: +Now you could run it with Python directly:
@@ -120,7 +168,7 @@ Hello Camila
-**Note**: auto-completion works when you create a Python package and run it with `--install-completion` or when you use Typer CLI. +**Note**: you can also call this same script with the `typer` command, but you don't need to. ## Example upgrade @@ -293,22 +341,43 @@ And similarly for **files**, **paths**, **enums** (choices), etc. And there are **You get**: great editor support, including **completion** and **type checks** everywhere. -**Your users get**: automatic **`--help`**, **auto-completion** in their terminal (Bash, Zsh, Fish, PowerShell) when they install your package or when using Typer CLI. +**Your users get**: automatic **`--help`**, **auto-completion** in their terminal (Bash, Zsh, Fish, PowerShell) when they install your package or when using the `typer` command. For a more complete example including more features, see the Tutorial - User Guide. -## Optional Dependencies +## Dependencies -Typer uses Click internally. That's the only dependency. +**Typer** stands on the shoulders of a giant. Its only internal required dependency is Click. -But you can also install extras: +By default it also comes with extra standard dependencies: -* rich: and Typer will show nicely formatted errors automatically. -* shellingham: and Typer will automatically detect the current shell when installing completion. +* rich: to show nicely formatted errors automatically. +* shellingham: to automatically detect the current shell when installing completion. * With `shellingham` you can just use `--install-completion`. * Without `shellingham`, you have to pass the name of the shell to install completion for, e.g. `--install-completion bash`. +* `typer-cli`: adds the `typer` command to your shell: + * Quickly run scripts (that don't have to use Typer) with shell completion. + * Generate docs for your Typer applications. + +### `typer-slim` + +If you don't want the extra dependencies, install `typer-slim` instead. + +When you install with: + +```bash +pip install typer +``` + +...it's the equivalent of: + +```bash +pip install "typer-slim[standard]" +``` + +The `standard` extra dependencies are `rich`, `shellingham`, `typer-cli`. -You can install `typer` with `rich` and `shellingham` with `pip install typer[all]`. +**Note**: even if you don't install `typer-cli` you can still use it's functionality by calling `typer` as a module, e.g. `python -m typer`. ## License diff --git a/docs/features.md b/docs/features.md index dc41191173..f1fb6adef6 100644 --- a/docs/features.md +++ b/docs/features.md @@ -49,16 +49,16 @@ The resulting CLI apps created with **Typer** have the nice features of many "pr * Automatic completion for the CLI app in all operating systems, in all the shells (Bash, Zsh, Fish, PowerShell), so that the final user of your app can just hit TAB and get the available options or subcommands. * !!! note "* Auto completion" - Auto completion works when you create a package (installable with `pip`). Or when using [Typer CLI](typer-cli.md){.internal-link target=_blank}. + Auto completion works when you create a package (installable with `pip`). Or when using the `typer` command. - If you also add `shellingham` as a dependency, **Typer** will use it to auto-detect the current shell when installing completion. + **Typer** uses `shellingham` to auto-detect the current shell when installing completion. If you don't want to include `shellingham`, install `typer-slim`. **Typer** will automatically create 2 *CLI options*: * `--install-completion`: Install completion for the current shell. * `--show-completion`: Show completion for the current shell, to copy it or customize the installation. - If you didn't add `shellingham` those *CLI options* take a value with the name of the shell to install completion for, e.g.: + If you didn't add `shellingham` (if you installed `pip install typer-slim`) those *CLI options* take a value with the name of the shell to install completion for, e.g.: * `--install-completion bash`. * `--show-completion powershell`. @@ -74,7 +74,7 @@ The resulting CLI apps created with **Typer** have the nice features of many "pr Click is one of the most popular tools for building CLIs in Python. -**Typer** is based on it, so you get all its benefits, plug-ins, robustness, etc. +**Typer** is based on it, so you get all its benefits. But you can write simpler code with the benefits of modern Python. diff --git a/docs/index.md b/docs/index.md index 303774709f..0d4d824aa1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -28,6 +28,8 @@ Typer is a library for building CLI applications that users will **love using** and developers will **love creating**. Based on Python type hints. +It's also a command line tool to run scripts, automatically converting them to CLI applications. + The key features are: * **Intuitive to write**: Great editor support. Completion everywhere. Less time debugging. Designed to be easy to use and learn. Less time reading docs. @@ -35,39 +37,87 @@ The key features are: * **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs. * **Start simple**: The simplest example adds only 2 lines of code to your app: **1 import, 1 function call**. * **Grow large**: Grow in complexity as much as you want, create arbitrarily complex trees of commands and groups of subcommands, with options and arguments. +* **Run scripts**: Typer includes a `typer` command that you can use to run scripts, automatically converting them to CLIs, even if they don't use Typer internally. ## FastAPI of CLIs - - -**Typer** is FastAPI's little sibling. - -And it's intended to be the FastAPI of CLIs. - -## Requirements - -**Typer** stands on the shoulders of a giant. Its only internal dependency is Click. +**Typer** is FastAPI's little sibling, it's the FastAPI of CLIs. ## Installation
```console -$ pip install "typer[all]" +$ pip install typer ---> 100% -Successfully installed typer +Successfully installed typer rich shellingham ```
-**Note**: that will include Rich. Rich is the recommended library to *display* information on the terminal, it is optional, but when installed, it's deeply integrated into **Typer** to display beautiful output. - ## Example ### The absolute minimum * Create a file `main.py` with: +```Python +def main(name: str): + print(f"Hello {name}") +``` + +This script doesn't even use Typer internally. But you can use the `typer` command to run it as a CLI application. + +### Run it + +Run your application with the `typer` command: + +
+ +```console +// Run your application +$ typer main.py run + +// You get a nice error, you are missing NAME +Usage: typer [PATH_OR_MODULE] run [OPTIONS] NAME +Try 'typer [PATH_OR_MODULE] run --help' for help. +╭─ Error ───────────────────────────────────────────╮ +│ Missing argument 'NAME'. │ +╰───────────────────────────────────────────────────╯ + + +// You get a --help for free +$ typer main.py run --help + +Usage: typer [PATH_OR_MODULE] run [OPTIONS] NAME + +Run the provided Typer app. + +╭─ Arguments ───────────────────────────────────────╮ +│ * name TEXT [default: None] [required] | +╰───────────────────────────────────────────────────╯ +╭─ Options ─────────────────────────────────────────╮ +│ --help Show this message and exit. │ +╰───────────────────────────────────────────────────╯ + +// Now pass the NAME argument +$ typer main.py run Camila + +Hello Camila + +// It works! 🎉 +``` + +
+ +This is the simplest use case, not even using Typer internally, but it can already be quite useful for simple scripts. + +**Note**: auto-completion works when you create a Python package and run it with `--install-completion` or when you use the `typer` command. + +## Use Typer in your code + +Now let's start using Typer in your own code, update `main.py` with: + ```Python import typer @@ -80,9 +130,7 @@ if __name__ == "__main__": typer.run(main) ``` -### Run it - -Run your application: +Now you could run it with Python directly:
@@ -120,7 +168,7 @@ Hello Camila
-**Note**: auto-completion works when you create a Python package and run it with `--install-completion` or when you use Typer CLI. +**Note**: you can also call this same script with the `typer` command, but you don't need to. ## Example upgrade @@ -293,22 +341,43 @@ And similarly for **files**, **paths**, **enums** (choices), etc. And there are **You get**: great editor support, including **completion** and **type checks** everywhere. -**Your users get**: automatic **`--help`**, **auto-completion** in their terminal (Bash, Zsh, Fish, PowerShell) when they install your package or when using Typer CLI. +**Your users get**: automatic **`--help`**, **auto-completion** in their terminal (Bash, Zsh, Fish, PowerShell) when they install your package or when using the `typer` command. For a more complete example including more features, see the Tutorial - User Guide. -## Optional Dependencies +## Dependencies -Typer uses Click internally. That's the only dependency. +**Typer** stands on the shoulders of a giant. Its only internal required dependency is Click. -But you can also install extras: +By default it also comes with extra standard dependencies: -* rich: and Typer will show nicely formatted errors automatically. -* shellingham: and Typer will automatically detect the current shell when installing completion. +* rich: to show nicely formatted errors automatically. +* shellingham: to automatically detect the current shell when installing completion. * With `shellingham` you can just use `--install-completion`. * Without `shellingham`, you have to pass the name of the shell to install completion for, e.g. `--install-completion bash`. +* `typer-cli`: adds the `typer` command to your shell: + * Quickly run scripts (that don't have to use Typer) with shell completion. + * Generate docs for your Typer applications. + +### `typer-slim` + +If you don't want the extra dependencies, install `typer-slim` instead. + +When you install with: + +```bash +pip install typer +``` + +...it's the equivalent of: + +```bash +pip install "typer-slim[standard]" +``` + +The `standard` extra dependencies are `rich`, `shellingham`, `typer-cli`. -You can install `typer` with `rich` and `shellingham` with `pip install typer[all]`. +**Note**: even if you don't install `typer-cli` you can still use it's functionality by calling `typer` as a module, e.g. `python -m typer`. ## License diff --git a/docs/tutorial/first-steps.md b/docs/tutorial/first-steps.md index 1f2371f0ab..6ef4212dde 100644 --- a/docs/tutorial/first-steps.md +++ b/docs/tutorial/first-steps.md @@ -452,11 +452,11 @@ And we'll use ***CLI option*** to refer to those *CLI parameters* that depend on We will use ***CLI parameter*** to refer to both, *CLI arguments* and *CLI options*. -## **Typer CLI** +## The `typer` Command -Now that you know the basics of **Typer**, you might want to install and use [Typer CLI](../typer-cli.md){.internal-link target=_blank}. +When you install `typer`, by default it adds a `typer` command to your shell. -**Typer CLI** is a tool to run your **Typer** scripts giving you ✨ auto completion ✨ in your terminal. +This `typer` command allows you to run your scripts with ✨ auto completion ✨ in your terminal. As an alternative to running with Python: @@ -470,7 +470,7 @@ Hello World -You can run with **Typer CLI**: +You can run with the `typer` command:
@@ -487,6 +487,6 @@ Hello World So you can use it to have auto completion for your own scripts as you continue with the tutorial. !!! tip - Your CLI application built with **Typer** won't need [Typer CLI](../typer-cli.md){.internal-link target=_blank} to have auto completion once you create a Python package. + Your CLI application built with **Typer** won't need the `typer` command to have auto completion once you create a Python package. But for short scripts and for learning, before creating a Python package, it might be useful. diff --git a/docs/tutorial/index.md b/docs/tutorial/index.md index 7937d570e4..6fb8075c87 100644 --- a/docs/tutorial/index.md +++ b/docs/tutorial/index.md @@ -61,18 +61,29 @@ You can learn a lot more by running some examples and playing around with them t ## Install **Typer** -The first step is to install **Typer**. - -For the tutorial, you might want to install it with all the optional dependencies and features: +The first step is to install **Typer**:
```console -$ pip install "typer[all]" +$ pip install typer ---> 100% -Successfully installed typer click shellingham rich +Successfully installed typer click shellingham rich typer-cli ```
-...that also includes `rich` and `shellingham`. +By default, `typer` comes with `rich`, `shellingham`, and `typer-cli`. + +!!! note + If you are an advanced user and want to opt out of these default extra dependencies, you can instead install `typer-slim`. + + ```bash + pip install typer + ``` + + ...is the equivalent of: + + ```bash + pip install "typer-slim[standard]" + ``` diff --git a/docs/tutorial/options-autocompletion.md b/docs/tutorial/options-autocompletion.md index a46c713172..7ac42dff8d 100644 --- a/docs/tutorial/options-autocompletion.md +++ b/docs/tutorial/options-autocompletion.md @@ -1,4 +1,4 @@ -As you have seen, apps built with **Typer** have completion in your shell that works when you create a Python package or using **Typer CLI**. +As you have seen, apps built with **Typer** have completion in your shell that works when you create a Python package or using the `typer` command. It normally completes *CLI options*, *CLI arguments*, and subcommands (that you will learn about later). @@ -8,9 +8,9 @@ But you can also provide auto completion for the **values** of *CLI options* and Before checking how to provide custom completions, let's check again how it works. -After installing completion (for your own Python package or for **Typer CLI**), when you use your CLI program and start adding a *CLI option* with `--` an then hit TAB, your shell will show you the available *CLI options* (the same for *CLI arguments*, etc). +After installing completion for your own Python package (or using the `typer` command), when you use your CLI program and start adding a *CLI option* with `--` an then hit TAB, your shell will show you the available *CLI options* (the same for *CLI arguments*, etc). -To check it quickly without creating a new Python package, install [Typer CLI](../typer-cli.md){.internal-link target=_blank}. +To check it quickly without creating a new Python package, use the `typer` command. Then let's create small example program: @@ -29,7 +29,7 @@ Then let's create small example program: {!> ../docs_src/options_autocompletion/tutorial001.py!} ``` -And let's try it with **Typer CLI** to get completion: +And let's try it with the `typer` command to get completion:
diff --git a/docs/tutorial/options/callback-and-context.md b/docs/tutorial/options/callback-and-context.md index 4c1c3a6527..4179eeff94 100644 --- a/docs/tutorial/options/callback-and-context.md +++ b/docs/tutorial/options/callback-and-context.md @@ -54,9 +54,9 @@ There's something to be aware of with callbacks and completion that requires som But first let's just use completion in your shell (Bash, Zsh, Fish, or PowerShell). -After installing completion (for your own Python package or for **Typer CLI**), when you use your CLI program and start adding a *CLI option* with `--` an then hit TAB, your shell will show you the available *CLI options* (the same for *CLI arguments*, etc). +After installing completion (for your own Python package), when you use your CLI program and start adding a *CLI option* with `--` an then hit TAB, your shell will show you the available *CLI options* (the same for *CLI arguments*, etc). -To check it quickly without creating a new Python package, install [Typer CLI](../../typer-cli.md){.internal-link target=_blank} and use it with the previous script: +To check it quickly with the previous script use the `typer` command:
diff --git a/docs/tutorial/package.md b/docs/tutorial/package.md index c26ff0087c..a6901a0a94 100644 --- a/docs/tutorial/package.md +++ b/docs/tutorial/package.md @@ -2,7 +2,7 @@ When you create a CLI program with **Typer** you probably want to create your ow That's what allows your users to install it and have it as an independent program that they can use in their terminal. -And that's also required for shell auto completion to work (unless you use your program through [Typer CLI](../typer-cli.md){.internal-link target=_blank}). +And that's also required for shell auto completion to work (unless you use your program through `typer` command). Nowadays, there are several ways and tools to create Python packages (what you install with `pip install something`). @@ -701,11 +701,9 @@ Loading portal gun
-## Generate docs with **Typer CLI** (optional) +## Generate docs -You can install and use [Typer CLI](../typer-cli.md){.internal-link target=_blank} to generate docs for your package. - -After installing it, you can use it to generate a new `README.md`: +You can use the `typer` command to generate docs for your package that you can put in your `README.md`:
@@ -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 Rich. - -### Install Rich - -First, you need to install it: - -
- -```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 -``` - -
+You can also display beautiful and more complex information using 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 -

- Run Typer scripts with completion, without having to create a package, using Typer CLI. -

-

- - Build Status - - - Coverage - - - Package version - -

- -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**: https://typer.tiangolo.com/typer-cli/ - -**Source Code for Typer CLI**: https://github.com/tiangolo/typer-cli - ---- - -## **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 +

+

+ Typer, build great CLIs. Easy to code. Based on Python type hints. +

+

+ + Test + + + Publish + + + Coverage + + Package version + +

+ +--- + +**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 = ["*"]