```console
-$ python -m rick_portal_gun
+$ python -m rick_portal_gun --help
Usage: __main__.py [OPTIONS] COMMAND [ARGS]...
@@ -513,7 +500,7 @@ You can pass all the arguments and keyword arguments you could pass to a Click a
```console
-$ python -m rick_portal_gun
+$ python -m rick_portal_gun --help
Usage: rick-portal-gun [OPTIONS] COMMAND [ARGS]...
@@ -603,7 +590,6 @@ $ poetry publish --build
Building rick-portal-gun (0.1.0)
- Building sdist
- Built rick-portal-gun-0.1.0.tar.gz
-
- Building wheel
- Built rick_portal_gun-0.1.0-py3-none-any.whl
@@ -630,10 +616,10 @@ $ pip uninstall rick-portal-gun
Found existing installation: rick-portal-gun 0.1.0
Uninstalling rick-portal-gun-0.1.0:
Would remove:
- /home/user/.local/bin/rick-portal-gun
- /home/user/.local/lib/python3.10/site-packages/rick_portal_gun-0.1.0.dist-info/*
- /home/user/.local/lib/python3.10/site-packages/rick_portal_gun/*
-# Proceed (y/n)? $ y
+ /home/rick/.local/bin/rick-portal-gun
+ /home/rick/.local/lib/python3.10/site-packages/rick_portal_gun-0.1.0.dist-info/*
+ /home/rick/.local/lib/python3.10/site-packages/rick_portal_gun/*
+# Proceed (Y/n)? $ Y
Successfully uninstalled rick-portal-gun-0.1.0
```
@@ -648,11 +634,16 @@ $ pip install --user rick-portal-gun
// Notice that it says "Downloading" 🚀
Collecting rick-portal-gun
- Downloading rick_portal_gun-0.1.0-py3-none-any.whl (1.8 kB)
-Requirement already satisfied: typer[all]<0.0.12,>=0.0.11 in ./.local/lib/python3.10/site-packages (from rick-portal-gun) (0.0.11)
-Requirement already satisfied: click<7.2.0,>=7.1.1 in ./anaconda3/lib/python3.10/site-packages (from typer[all]<0.0.12,>=0.0.11->rick-portal-gun) (7.1.1)
-Requirement already satisfied: colorama; extra == "all" in ./anaconda3/lib/python3.10/site-packages (from typer[all]<0.0.12,>=0.0.11->rick-portal-gun) (0.4.3)
-Requirement already satisfied: shellingham; extra == "all" in ./anaconda3/lib/python3.10/site-packages (from typer[all]<0.0.12,>=0.0.11->rick-portal-gun) (1.3.1)
+ Downloading rick_portal_gun-0.1.0-py3-none-any.whl.metadata (435 bytes)
+Requirement already satisfied: typer<0.13.0,>=0.12.3 in ./.local/lib/python3.10/site-packages (from rick-portal-gun==0.1.0) (0.12.3)
+Requirement already satisfied: typing-extensions>=3.7.4.3 in ./.local/lib/python3.10/site-packages (from typer<0.13.0,>=0.12.3->rick-portal-gun==0.1.0) (4.11.0)
+Requirement already satisfied: click>=8.0.0 in ./.local/lib/python3.10/site-packages (from typer<0.13.0,>=0.12.3->rick-portal-gun==0.1.0) (8.1.7)
+Requirement already satisfied: shellingham>=1.3.0 in ./.local/lib/python3.10/site-packages (from typer<0.13.0,>=0.12.3->rick-portal-gun==0.1.0) (1.5.4)
+Requirement already satisfied: rich>=10.11.0 in ./.local/lib/python3.10/site-packages (from typer<0.13.0,>=0.12.3->rick-portal-gun==0.1.0) (13.7.1)
+Requirement already satisfied: pygments<3.0.0,>=2.13.0 in ./.local/lib/python3.10/site-packages (from rich>=10.11.0->typer<0.13.0,>=0.12.3->rick-portal-gun==0.1.0) (2.17.2)
+Requirement already satisfied: markdown-it-py>=2.2.0 in ./.local/lib/python3.10/site-packages (from rich>=10.11.0->typer<0.13.0,>=0.12.3->rick-portal-gun==0.1.0) (3.0.0)
+Requirement already satisfied: mdurl~=0.1 in ./.local/lib/python3.10/site-packages (from markdown-it-py>=2.2.0->rich>=10.11.0->typer<0.13.0,>=0.12.3->rick-portal-gun==0.1.0) (0.1.2)
+Downloading rick_portal_gun-0.1.0-py3-none-any.whl (1.8 kB)
Installing collected packages: rick-portal-gun
Successfully installed rick-portal-gun-0.1.0
```
@@ -715,14 +706,11 @@ rick-portal-gun = "rick_portal_gun.main:app"
[tool.poetry.dependencies]
python = "^3.10"
-typer = {extras = ["all"], version = "^0.1.0"}
-
-[tool.poetry.dev-dependencies]
-pytest = "^5.2"
+typer = "^0.12.0"
[build-system]
-requires = ["poetry>=0.12"]
-build-backend = "poetry.masonry.api"
+requires = ["poetry-core"]
+build-backend = "poetry.core.masonry.api"
```
And in the file `rick_portal_gun/__init__.py`:
@@ -743,7 +731,6 @@ $ poetry publish --build
Building rick-portal-gun (0.2.0)
- Building sdist
- Built rick-portal-gun-0.2.0.tar.gz
-
- Building wheel
- Built rick_portal_gun-0.2.0-py3-none-any.whl
diff --git a/docs/tutorial/progressbar.md b/docs/tutorial/progressbar.md
index 250ea49382..c56cf50b29 100644
--- a/docs/tutorial/progressbar.md
+++ b/docs/tutorial/progressbar.md
@@ -246,5 +246,5 @@ Check it:
python main.py
-Processed 100 things in batches.
+Processed 1000 things in batches.
diff --git a/docs/virtual-environments.md b/docs/virtual-environments.md
new file mode 100644
index 0000000000..d4db3a77c3
--- /dev/null
+++ b/docs/virtual-environments.md
@@ -0,0 +1,844 @@
+# Virtual Environments
+
+When you work in Python projects you probably should use a **virtual environment** (or a similar mechanism) to isolate the packages you install for each project.
+
+/// info
+
+If you already know about virtual environments, how to create them and use them, you might want to skip this section. 🤓
+
+///
+
+/// tip
+
+A **virtual environment** is different than an **environment variable**.
+
+An **environment variable** is a variable in the system that can be used by programs.
+
+A **virtual environment** is a directory with some files in it.
+
+///
+
+/// info
+
+This page will teach you how to use **virtual environments** and how they work.
+
+If you are ready to adopt a **tool that manages everything** for you (including installing Python), try
uv.
+
+///
+
+## Create a Project
+
+First, create a directory for your project.
+
+What I normally do is that I create a directory named `code` inside my home/user directory.
+
+And inside of that I create one directory per project.
+
+
+
+```console
+// Go to the home directory
+$ cd
+// Create a directory for all your code projects
+$ mkdir code
+// Enter into that code directory
+$ cd code
+// Create a directory for this project
+$ mkdir awesome-project
+// Enter into that project directory
+$ cd awesome-project
+```
+
+
+
+## Create a Virtual Environment
+
+When you start working on a Python project **for the first time**, create a virtual environment **
inside your project**.
+
+/// tip
+
+You only need to do this **once per project**, not every time you work.
+
+///
+
+//// tab | `venv`
+
+To create a virtual environment, you can use the `venv` module that comes with Python.
+
+
+
+```console
+$ python -m venv .venv
+```
+
+
+
+/// details | What that command means
+
+* `python`: use the program called `python`
+* `-m`: call a module as a script, we'll tell it which module next
+* `venv`: use the module called `venv` that normally comes installed with Python
+* `.venv`: create the virtual environment in the new directory `.venv`
+
+///
+
+////
+
+//// tab | `uv`
+
+If you have
`uv` installed, you can use it to create a virtual environment.
+
+
+
+```console
+$ uv venv
+```
+
+
+
+/// tip
+
+By default, `uv` will create a virtual environment in a directory called `.venv`.
+
+But you could customize it passing an additional argument with the directory name.
+
+///
+
+////
+
+That command creates a new virtual environment in a directory called `.venv`.
+
+/// details | `.venv` or other name
+
+You could create the virtual environment in a different directory, but there's a convention of calling it `.venv`.
+
+///
+
+## Activate the Virtual Environment
+
+Activate the new virtual environment so that any Python command you run or package you install uses it.
+
+/// tip
+
+Do this **every time** you start a **new terminal session** to work on the project.
+
+///
+
+//// tab | Linux, macOS
+
+
+
+```console
+$ source .venv/bin/activate
+```
+
+
+
+////
+
+//// tab | Windows PowerShell
+
+
+
+```console
+$ .venv\Scripts\Activate.ps1
+```
+
+
+
+////
+
+//// tab | Windows Bash
+
+Or if you use Bash for Windows (e.g.
Git Bash):
+
+
+
+```console
+$ source .venv/Scripts/activate
+```
+
+
+
+////
+
+/// tip
+
+Every time you install a **new package** in that environment, **activate** the environment again.
+
+This makes sure that if you use a **terminal (
CLI) program** installed by that package, you use the one from your virtual environment and not any other that could be installed globally, probably with a different version than what you need.
+
+///
+
+## Check the Virtual Environment is Active
+
+Check that the virtual environment is active (the previous command worked).
+
+/// tip
+
+This is **optional**, but it's a good way to **check** that everything is working as expected and you are using the virtual environment you intended.
+
+///
+
+//// tab | Linux, macOS, Windows Bash
+
+
+
+```console
+$ which python
+
+/home/user/code/awesome-project/.venv/bin/python
+```
+
+
+
+If it shows the `python` binary at `.venv/bin/python`, inside of your project (in this case `awesome-project`), then it worked. 🎉
+
+////
+
+//// tab | Windows PowerShell
+
+
+
+```console
+$ Get-Command python
+
+C:\Users\user\code\awesome-project\.venv\Scripts\python
+```
+
+
+
+If it shows the `python` binary at `.venv\Scripts\python`, inside of your project (in this case `awesome-project`), then it worked. 🎉
+
+////
+
+## Upgrade `pip`
+
+/// tip
+
+If you use
`uv` you would use it to install things instead of `pip`, so you don't need to upgrade `pip`. 😎
+
+///
+
+If you are using `pip` to install packages (it comes by default with Python), you should **upgrade** it to the latest version.
+
+Many exotic errors while installing a package are solved by just upgrading `pip` first.
+
+/// tip
+
+You would normally do this **once**, right after you create the virtual environment.
+
+///
+
+Make sure the virtual environment is active (with the command above) and then run:
+
+
+
+```console
+$ python -m pip install --upgrade pip
+
+---> 100%
+```
+
+
+
+## Add `.gitignore`
+
+If you are using **Git** (you should), add a `.gitignore` file to exclude everything in your `.venv` from Git.
+
+/// tip
+
+If you used
`uv` to create the virtual environment, it already did this for you, you can skip this step. 😎
+
+///
+
+/// tip
+
+Do this **once**, right after you create the virtual environment.
+
+///
+
+
+
+```console
+$ echo "*" > .venv/.gitignore
+```
+
+
+
+/// details | What that command means
+
+* `echo "*"`: will "print" the text `*` in the terminal (the next part changes that a bit)
+* `>`: anything printed to the terminal by the command to the left of `>` should not be printed but instead written to the file that goes to the right of `>`
+* `.gitignore`: the name of the file where the text should be written
+
+And `*` for Git means "everything". So, it will ignore everything in the `.venv` directory.
+
+That command will create a file `.gitignore` with the content:
+
+```gitignore
+*
+```
+
+///
+
+## Install Packages
+
+After activating the environment, you can install packages in it.
+
+/// tip
+
+Do this **once** when installing or upgrading the packages your project needs.
+
+If you need to upgrade a version or add a new package you would **do this again**.
+
+///
+
+### Install Packages Directly
+
+If you're in a hurry and don't want to use a file to declare your project's package requirements, you can install them directly.
+
+/// tip
+
+It's a (very) good idea to put the packages and versions your program needs in a file (for example `requirements.txt` or `pyproject.toml`).
+
+///
+
+//// tab | `pip`
+
+
+
+```console
+$ pip install typer
+
+---> 100%
+```
+
+
+
+////
+
+//// tab | `uv`
+
+If you have
`uv`:
+
+
+
+```console
+$ uv pip install typer
+---> 100%
+```
+
+
+
+////
+
+### Install from `requirements.txt`
+
+If you have a `requirements.txt`, you can now use it to install its packages.
+
+//// tab | `pip`
+
+
+
+```console
+$ pip install -r requirements.txt
+---> 100%
+```
+
+
+
+////
+
+//// tab | `uv`
+
+If you have
`uv`:
+
+
+
+```console
+$ uv pip install -r requirements.txt
+---> 100%
+```
+
+
+
+////
+
+/// details | `requirements.txt`
+
+A `requirements.txt` with some packages could look like:
+
+```requirements.txt
+typer==0.13.0
+rich==13.7.1
+```
+
+///
+
+## Run Your Program
+
+After you activated the virtual environment, you can run your program, and it will use the Python inside of your virtual environment with the packages you installed there.
+
+
+
+```console
+$ python main.py
+
+Hello World
+```
+
+
+
+## Configure Your Editor
+
+You would probably use an editor, make sure you configure it to use the same virtual environment you created (it will probably autodetect it) so that you can get autocompletion and inline errors.
+
+For example:
+
+*
VS Code
+*
PyCharm
+
+/// tip
+
+You normally have to do this only **once**, when you create the virtual environment.
+
+///
+
+## Deactivate the Virtual Environment
+
+Once you are done working on your project you can **deactivate** the virtual environment.
+
+
+
+```console
+$ deactivate
+```
+
+
+
+This way, when you run `python` it won't try to run it from that virtual environment with the packages installed there.
+
+## Ready to Work
+
+Now you're ready to start working on your project.
+
+
+
+/// tip
+
+Do you want to understand what's all that above?
+
+Continue reading. 👇🤓
+
+///
+
+## Why Virtual Environments
+
+To work with Typer you need to install
Python.
+
+After that, you would need to **install** Typer and any other **packages** you want to use.
+
+To install packages you would normally use the `pip` command that comes with Python (or similar alternatives).
+
+Nevertheless, if you just use `pip` directly, the packages would be installed in your **global Python environment** (the global installation of Python).
+
+### The Problem
+
+So, what's the problem with installing packages in the global Python environment?
+
+At some point, you will probably end up writing many different programs that depend on **different packages**. And some of these projects you work on will depend on **different versions** of the same package. 😱
+
+For example, you could create a project called `philosophers-stone`, this program depends on another package called **`harry`, using the version `1`**. So, you need to install `harry`.
+
+```mermaid
+flowchart LR
+ stone(philosophers-stone) -->|requires| harry-1[harry v1]
+```
+
+Then, at some point later, you create another project called `prisoner-of-azkaban`, and this project also depends on `harry`, but this project needs **`harry` version `3`**.
+
+```mermaid
+flowchart LR
+ azkaban(prisoner-of-azkaban) --> |requires| harry-3[harry v3]
+```
+
+But now the problem is, if you install the packages globally (in the global environment) instead of in a local **virtual environment**, you will have to choose which version of `harry` to install.
+
+If you want to run `philosophers-stone` you will need to first install `harry` version `1`, for example with:
+
+
+
+```console
+$ pip install "harry==1"
+```
+
+
+
+And then you would end up with `harry` version `1` installed in your global Python environment.
+
+```mermaid
+flowchart LR
+ subgraph global[global env]
+ harry-1[harry v1]
+ end
+ subgraph stone-project[philosophers-stone project]
+ stone(philosophers-stone) -->|requires| harry-1
+ end
+```
+
+But then if you want to run `prisoner-of-azkaban`, you will need to uninstall `harry` version `1` and install `harry` version `3` (or just installing version `3` would automatically uninstall version `1`).
+
+
+
+```console
+$ pip install "harry==3"
+```
+
+
+
+And then you would end up with `harry` version `3` installed in your global Python environment.
+
+And if you try to run `philosophers-stone` again, there's a chance it would **not work** because it needs `harry` version `1`.
+
+```mermaid
+flowchart LR
+ subgraph global[global env]
+ harry-1[
harry v1]
+ style harry-1 fill:#ccc,stroke-dasharray: 5 5
+ harry-3[harry v3]
+ end
+ subgraph stone-project[philosophers-stone project]
+ stone(philosophers-stone) -.-x|⛔️| harry-1
+ end
+ subgraph azkaban-project[prisoner-of-azkaban project]
+ azkaban(prisoner-of-azkaban) --> |requires| harry-3
+ end
+```
+
+/// tip
+
+It's very common in Python packages to try the best to **avoid breaking changes** in **new versions**, but it's better to be safe, and install newer versions intentionally and when you can run the tests to check everything is working correctly.
+
+///
+
+Now, imagine that with **many** other **packages** that all your **projects depend on**. That's very difficult to manage. And you would probably end up running some projects with some **incompatible versions** of the packages, and not knowing why something isn't working.
+
+Also, depending on your operating system (e.g. Linux, Windows, macOS), it could have come with Python already installed. And in that case it probably had some packages pre-installed with some specific versions **needed by your system**. If you install packages in the global Python environment, you could end up **breaking** some of the programs that came with your operating system.
+
+## Where are Packages Installed
+
+When you install Python, it creates some directories with some files in your computer.
+
+Some of these directories are the ones in charge of having all the packages you install.
+
+When you run:
+
+
+
+```console
+// Don't run this now, it's just an example 🤓
+$ pip install typer
+---> 100%
+```
+
+
+
+That will download a compressed file with the Typer code, normally from
PyPI.
+
+It will also **download** files for other packages that Typer depends on.
+
+Then it will **extract** all those files and put them in a directory in your computer.
+
+By default, it will put those files downloaded and extracted in the directory that comes with your Python installation, that's the **global environment**.
+
+## What are Virtual Environments
+
+The solution to the problems of having all the packages in the global environment is to use a **virtual environment for each project** you work on.
+
+A virtual environment is a **directory**, very similar to the global one, where you can install the packages for a project.
+
+This way, each project will have its own virtual environment (`.venv` directory) with its own packages.
+
+```mermaid
+flowchart TB
+ subgraph stone-project[philosophers-stone project]
+ stone(philosophers-stone) --->|requires| harry-1
+ subgraph venv1[.venv]
+ harry-1[harry v1]
+ end
+ end
+ subgraph azkaban-project[prisoner-of-azkaban project]
+ azkaban(prisoner-of-azkaban) --->|requires| harry-3
+ subgraph venv2[.venv]
+ harry-3[harry v3]
+ end
+ end
+ stone-project ~~~ azkaban-project
+```
+
+## What Does Activating a Virtual Environment Mean
+
+When you activate a virtual environment, for example with:
+
+//// tab | Linux, macOS
+
+
+
+```console
+$ source .venv/bin/activate
+```
+
+
+
+////
+
+//// tab | Windows PowerShell
+
+
+
+```console
+$ .venv\Scripts\Activate.ps1
+```
+
+
+
+////
+
+//// tab | Windows Bash
+
+Or if you use Bash for Windows (e.g.
Git Bash):
+
+
+
+```console
+$ source .venv/Scripts/activate
+```
+
+
+
+////
+
+That command will create or modify some [environment variables](environment-variables.md){.internal-link target=_blank} that will be available for the next commands.
+
+One of those variables is the `PATH` variable.
+
+/// tip
+
+You can learn more about the `PATH` environment variable in the [Environment Variables](environment-variables.md#path-environment-variable){.internal-link target=_blank} section.
+
+///
+
+Activating a virtual environment adds its path `.venv/bin` (on Linux and macOS) or `.venv\Scripts` (on Windows) to the `PATH` environment variable.
+
+Let's say that before activating the environment, the `PATH` variable looked like this:
+
+//// tab | Linux, macOS
+
+```plaintext
+/usr/bin:/bin:/usr/sbin:/sbin
+```
+
+That means that the system would look for programs in:
+
+* `/usr/bin`
+* `/bin`
+* `/usr/sbin`
+* `/sbin`
+
+////
+
+//// tab | Windows
+
+```plaintext
+C:\Windows\System32
+```
+
+That means that the system would look for programs in:
+
+* `C:\Windows\System32`
+
+////
+
+After activating the virtual environment, the `PATH` variable would look something like this:
+
+//// tab | Linux, macOS
+
+```plaintext
+/home/user/code/awesome-project/.venv/bin:/usr/bin:/bin:/usr/sbin:/sbin
+```
+
+That means that the system will now start looking first look for programs in:
+
+```plaintext
+/home/user/code/awesome-project/.venv/bin
+```
+
+before looking in the other directories.
+
+So, when you type `python` in the terminal, the system will find the Python program in
+
+```plaintext
+/home/user/code/awesome-project/.venv/bin/python
+```
+
+and use that one.
+
+////
+
+//// tab | Windows
+
+```plaintext
+C:\Users\user\code\awesome-project\.venv\Scripts;C:\Windows\System32
+```
+
+That means that the system will now start looking first look for programs in:
+
+```plaintext
+C:\Users\user\code\awesome-project\.venv\Scripts
+```
+
+before looking in the other directories.
+
+So, when you type `python` in the terminal, the system will find the Python program in
+
+```plaintext
+C:\Users\user\code\awesome-project\.venv\Scripts\python
+```
+
+and use that one.
+
+////
+
+An important detail is that it will put the virtual environment path at the **beginning** of the `PATH` variable. The system will find it **before** finding any other Python available. This way, when you run `python`, it will use the Python **from the virtual environment** instead of any other `python` (for example, a `python` from a global environment).
+
+Activating a virtual environment also changes a couple of other things, but this is one of the most important things it does.
+
+## Checking a Virtual Environment
+
+When you check if a virtual environment is active, for example with:
+
+//// tab | Linux, macOS, Windows Bash
+
+
+
+```console
+$ which python
+
+/home/user/code/awesome-project/.venv/bin/python
+```
+
+
+
+////
+
+//// tab | Windows PowerShell
+
+
+
+```console
+$ Get-Command python
+
+C:\Users\user\code\awesome-project\.venv\Scripts\python
+```
+
+
+
+////
+
+That means that the `python` program that will be used is the one **in the virtual environment**.
+
+you use `which` in Linux and macOS and `Get-Command` in Windows PowerShell.
+
+The way that command works is that it will go and check in the `PATH` environment variable, going through **each path in order**, looking for the program called `python`. Once it finds it, it will **show you the path** to that program.
+
+The most important part is that when you call `python`, that is the exact "`python`" that will be executed.
+
+So, you can confirm if you are in the correct virtual environment.
+
+/// tip
+
+It's easy to activate one virtual environment, get one Python, and then **go to another project**.
+
+And the second project **wouldn't work** because you are using the **incorrect Python**, from a virtual environment for another project.
+
+It's useful being able to check what `python` is being used. 🤓
+
+///
+
+## Why Deactivate a Virtual Environment
+
+For example, you could be working on a project `philosophers-stone`, **activate that virtual environment**, install packages and work with that environment.
+
+And then you want to work on **another project** `prisoner-of-azkaban`.
+
+You go to that project:
+
+
+
+```console
+$ cd ~/code/prisoner-of-azkaban
+```
+
+
+
+If you don't deactivate the virtual environment for `philosophers-stone`, when you run `python` in the terminal, it will try to use the Python from `philosophers-stone`.
+
+
+
+```console
+$ cd ~/code/prisoner-of-azkaban
+
+$ python main.py
+
+// Error importing sirius, it's not installed 😱
+Traceback (most recent call last):
+ File "main.py", line 1, in
+ import sirius
+```
+
+
+
+But if you deactivate the virtual environment and activate the new one for `prisoner-of-askaban` then when you run `python` it will use the Python from the virtual environment in `prisoner-of-azkaban`.
+
+
+
+```console
+$ cd ~/code/prisoner-of-azkaban
+
+// You don't need to be in the old directory to deactivate, you can do it wherever you are, even after going to the other project 😎
+$ deactivate
+
+// Activate the virtual environment in prisoner-of-azkaban/.venv 🚀
+$ source .venv/bin/activate
+
+// Now when you run python, it will find the package sirius installed in this virtual environment ✨
+$ python main.py
+
+I solemnly swear 🐺
+```
+
+
+
+## Alternatives
+
+This is a simple guide to get you started and teach you how everything works **underneath**.
+
+There are many **alternatives** to managing virtual environments, package dependencies (requirements), projects.
+
+Once you are ready and want to use a tool to **manage the entire project**, packages dependencies, virtual environments, etc. I would suggest you try
uv.
+
+`uv` can do a lot of things, it can:
+
+* **Install Python** for you, including different versions
+* Manage the **virtual environment** for your projects
+* Install **packages**
+* Manage package **dependencies and versions** for your project
+* Make sure you have an **exact** set of packages and versions to install, including their dependencies, so that you can be sure that you can run your project in production exactly the same as in your computer while developing, this is called **locking**
+* And many other things
+
+## Conclusion
+
+If you read and understood all this, now **you know much more** about virtual environments than many developers out there. 🤓
+
+Knowing these details will most probably be useful in a future time when you are debugging something that seems complex, but you will know **how it all works underneath**. 😎
diff --git a/docs_src/commands/index/tutorial004.py b/docs_src/commands/index/tutorial004.py
new file mode 100644
index 0000000000..83419b695f
--- /dev/null
+++ b/docs_src/commands/index/tutorial004.py
@@ -0,0 +1,17 @@
+import typer
+
+app = typer.Typer()
+
+
+@app.command()
+def delete():
+ print("Deleting user: Hiro Hamada")
+
+
+@app.command()
+def create():
+ print("Creating user: Hiro Hamada")
+
+
+if __name__ == "__main__":
+ app()
diff --git a/docs_src/progressbar/tutorial006.py b/docs_src/progressbar/tutorial006.py
index ac94a3ed3e..d83b0da7cb 100644
--- a/docs_src/progressbar/tutorial006.py
+++ b/docs_src/progressbar/tutorial006.py
@@ -9,6 +9,8 @@ def main():
for batch in range(4):
# Fake processing time
time.sleep(1)
+ # Increment by 250 on each loop iteration
+ # (it will take 4 seconds to reach 1000)
progress.update(250)
print(f"Processed {total} things in batches.")
diff --git a/docs_src/using_click/tutorial001.py b/docs_src/using_click/tutorial001.py
index 6fbf00649a..b3260c6494 100644
--- a/docs_src/using_click/tutorial001.py
+++ b/docs_src/using_click/tutorial001.py
@@ -7,7 +7,7 @@
def hello(count, name):
"""Simple program that greets NAME for a total of COUNT times."""
for x in range(count):
- click.echo("Hello %s!" % name)
+ click.echo(f"Hello {name}!")
if __name__ == "__main__":
diff --git a/docs_src/using_click/tutorial003.py b/docs_src/using_click/tutorial003.py
index a8f99e4623..5b3967c015 100644
--- a/docs_src/using_click/tutorial003.py
+++ b/docs_src/using_click/tutorial003.py
@@ -23,7 +23,7 @@ def callback():
@click.option("--name", prompt="Your name", help="The person to greet.")
def hello(name):
"""Simple program that greets NAME for a total of COUNT times."""
- click.echo("Hello %s!" % name)
+ click.echo(f"Hello {name}!")
typer_click_object = typer.main.get_command(app)
diff --git a/mkdocs.yml b/mkdocs.yml
index cf15b162d9..ead95508a0 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -72,6 +72,9 @@ nav:
- features.md
- Tutorial - User Guide:
- tutorial/index.md
+ - environment-variables.md
+ - virtual-environments.md
+ - tutorial/install.md
- tutorial/first-steps.md
- tutorial/printing.md
- tutorial/terminating.md
diff --git a/pyproject.toml b/pyproject.toml
index b64b18d911..ce9d61afa3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -194,9 +194,6 @@ ignore = [
# Loop control variable `x` not used within loop body
"docs_src/using_click/tutorial001.py" = ["B007"]
-# TODO: refactor _typing.py, remove unnecessary code
-"typer/_typing.py" = ["UP036", "F822"]
-
[tool.ruff.lint.isort]
known-third-party = ["typer", "click"]
# For docs_src/subcommands/tutorial003/
diff --git a/requirements-docs.txt b/requirements-docs.txt
index 4ad8149417..ddee32cb92 100644
--- a/requirements-docs.txt
+++ b/requirements-docs.txt
@@ -1,13 +1,13 @@
-e .
-mkdocs-material==9.5.18
+mkdocs-material==9.5.34
mdx-include >=1.4.1,<2.0.0
mkdocs-redirects>=1.2.1,<1.3.0
pyyaml >=5.3.1,<7.0.0
# For Material for MkDocs, Chinese search
# jieba==0.42.1
# For image processing by Material for MkDocs
-pillow==10.3.0
+pillow==10.4.0
# For image processing by Material for MkDocs
cairosvg==2.7.1
# mkdocstrings[python]==0.25.1
diff --git a/requirements-tests.txt b/requirements-tests.txt
index ac6377c10d..df1aeb1f61 100644
--- a/requirements-tests.txt
+++ b/requirements-tests.txt
@@ -6,7 +6,7 @@ coverage[toml] >=6.2,<8.0
pytest-xdist >=1.32.0,<4.0.0
pytest-sugar >=0.9.4,<1.1.0
mypy ==1.4.1
-ruff ==0.2.0
+ruff ==0.6.4
# Needed explicitly by typer-slim
rich >=10.11.0
shellingham >=1.3.0
diff --git a/scripts/docker/Dockerfile b/scripts/docker/Dockerfile
new file mode 100644
index 0000000000..b158e2db04
--- /dev/null
+++ b/scripts/docker/Dockerfile
@@ -0,0 +1,28 @@
+FROM python:latest
+
+# Add Fish
+RUN echo 'deb http://download.opensuse.org/repositories/shells:/fish:/release:/3/Debian_12/ /' | tee /etc/apt/sources.list.d/shells:fish:release:3.list
+RUN curl -fsSL https://download.opensuse.org/repositories/shells:fish:release:3/Debian_12/Release.key | gpg --dearmor | tee /etc/apt/trusted.gpg.d/shells_fish_release_3.gpg > /dev/null
+
+# Install packages including Fish, Zsh, PowerShell
+RUN apt-get update && apt-get install -y \
+ wget \
+ apt-transport-https \
+ software-properties-common \
+ nano \
+ vim \
+ fish \
+ zsh \
+ && wget https://github.com/PowerShell/PowerShell/releases/download/v7.4.4/powershell_7.4.4-1.deb_amd64.deb \
+ && dpkg -i powershell_7.4.4-1.deb_amd64.deb
+
+# Install uv
+COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
+
+ENV UV_SYSTEM_PYTHON=1
+
+COPY . /code
+
+WORKDIR /code
+
+RUN uv pip install -r requirements.txt
diff --git a/scripts/docker/compose.yaml b/scripts/docker/compose.yaml
new file mode 100644
index 0000000000..efe17f9efd
--- /dev/null
+++ b/scripts/docker/compose.yaml
@@ -0,0 +1,8 @@
+services:
+ typer:
+ build:
+ context: ../../
+ dockerfile: scripts/docker/Dockerfile
+ volumes:
+ - ../../:/code
+ command: sleep infinity
diff --git a/tests/assets/cli/multiapp-docs-title.md b/tests/assets/cli/multiapp-docs-title.md
index e688a9ba91..ffde843736 100644
--- a/tests/assets/cli/multiapp-docs-title.md
+++ b/tests/assets/cli/multiapp-docs-title.md
@@ -18,41 +18,41 @@ The end
**Commands**:
-* `sub`
* `top`: Top command
+* `sub`
-## `multiapp sub`
+## `multiapp top`
+
+Top command
**Usage**:
```console
-$ multiapp sub [OPTIONS] COMMAND [ARGS]...
+$ multiapp top [OPTIONS]
```
**Options**:
* `--help`: Show this message and exit.
-**Commands**:
-
-* `bye`: Say bye
-* `hello`: Say Hello
-* `hi`: Say Hi
-
-### `multiapp sub bye`
-
-Say bye
+## `multiapp sub`
**Usage**:
```console
-$ multiapp sub bye [OPTIONS]
+$ multiapp sub [OPTIONS] COMMAND [ARGS]...
```
**Options**:
* `--help`: Show this message and exit.
+**Commands**:
+
+* `hello`: Say Hello
+* `hi`: Say Hi
+* `bye`: Say bye
+
### `multiapp sub hello`
Say Hello
@@ -87,14 +87,14 @@ $ multiapp sub hi [OPTIONS] [USER]
* `--help`: Show this message and exit.
-## `multiapp top`
+### `multiapp sub bye`
-Top command
+Say bye
**Usage**:
```console
-$ multiapp top [OPTIONS]
+$ multiapp sub bye [OPTIONS]
```
**Options**:
diff --git a/tests/assets/cli/multiapp-docs.md b/tests/assets/cli/multiapp-docs.md
index ed4592f5c8..67d02568db 100644
--- a/tests/assets/cli/multiapp-docs.md
+++ b/tests/assets/cli/multiapp-docs.md
@@ -18,41 +18,41 @@ The end
**Commands**:
-* `sub`
* `top`: Top command
+* `sub`
-## `multiapp sub`
+## `multiapp top`
+
+Top command
**Usage**:
```console
-$ multiapp sub [OPTIONS] COMMAND [ARGS]...
+$ multiapp top [OPTIONS]
```
**Options**:
* `--help`: Show this message and exit.
-**Commands**:
-
-* `bye`: Say bye
-* `hello`: Say Hello
-* `hi`: Say Hi
-
-### `multiapp sub bye`
-
-Say bye
+## `multiapp sub`
**Usage**:
```console
-$ multiapp sub bye [OPTIONS]
+$ multiapp sub [OPTIONS] COMMAND [ARGS]...
```
**Options**:
* `--help`: Show this message and exit.
+**Commands**:
+
+* `hello`: Say Hello
+* `hi`: Say Hi
+* `bye`: Say bye
+
### `multiapp sub hello`
Say Hello
@@ -87,14 +87,14 @@ $ multiapp sub hi [OPTIONS] [USER]
* `--help`: Show this message and exit.
-## `multiapp top`
+### `multiapp sub bye`
-Top command
+Say bye
**Usage**:
```console
-$ multiapp top [OPTIONS]
+$ multiapp sub bye [OPTIONS]
```
**Options**:
diff --git a/tests/test_ambiguous_params.py b/tests/test_ambiguous_params.py
index 4dbf09b569..0693c8e9aa 100644
--- a/tests/test_ambiguous_params.py
+++ b/tests/test_ambiguous_params.py
@@ -29,8 +29,7 @@ def test_forbid_default_value_in_annotated_argument():
# This test case only works with `typer.Argument`. `typer.Option` uses positionals
# for param_decls too.
@app.command()
- def cmd(my_param: Annotated[str, typer.Argument("foo")]):
- ... # pragma: no cover
+ def cmd(my_param: Annotated[str, typer.Argument("foo")]): ... # pragma: no cover
with pytest.raises(AnnotatedParamWithDefaultValueError) as excinfo:
runner.invoke(app)
@@ -64,8 +63,7 @@ def test_forbid_annotated_param_and_default_param(param, param_info_type):
app = typer.Typer()
@app.command()
- def cmd(my_param: Annotated[str, param()] = param("foo")):
- ... # pragma: no cover
+ def cmd(my_param: Annotated[str, param()] = param("foo")): ... # pragma: no cover
with pytest.raises(MixedAnnotatedAndDefaultStyleError) as excinfo:
runner.invoke(app)
@@ -83,8 +81,7 @@ def test_forbid_multiple_typer_params_in_annotated():
@app.command()
def cmd(
my_param: Annotated[str, typer.Argument(), typer.Argument()],
- ):
- ... # pragma: no cover
+ ): ... # pragma: no cover
with pytest.raises(MultipleTyperAnnotationsError) as excinfo:
runner.invoke(app)
@@ -121,8 +118,7 @@ def make_string():
@app.command()
def cmd(
my_param: Annotated[str, param(default_factory=make_string)] = "hello",
- ):
- ... # pragma: no cover
+ ): ... # pragma: no cover
with pytest.raises(DefaultFactoryAndDefaultValueError) as excinfo:
runner.invoke(app)
@@ -171,8 +167,7 @@ def make_string():
@app.command()
def cmd(
my_param: str = param("hi", default_factory=make_string),
- ):
- ... # pragma: no cover
+ ): ... # pragma: no cover
with pytest.raises(DefaultFactoryAndDefaultValueError) as excinfo:
runner.invoke(app)
diff --git a/tests/test_completion/test_completion.py b/tests/test_completion/test_completion.py
index 703373b226..36581aba09 100644
--- a/tests/test_completion/test_completion.py
+++ b/tests/test_completion/test_completion.py
@@ -5,9 +5,10 @@
from docs_src.commands.index import tutorial001 as mod
-from ..utils import needs_linux
+from ..utils import needs_bash, needs_linux
+@needs_bash
@needs_linux
def test_show_completion():
result = subprocess.run(
@@ -23,6 +24,7 @@ def test_show_completion():
assert "_TUTORIAL001.PY_COMPLETE=complete_bash" in result.stdout
+@needs_bash
@needs_linux
def test_install_completion():
bash_completion_path: Path = Path.home() / ".bashrc"
diff --git a/tests/test_completion/test_completion_install.py b/tests/test_completion/test_completion_install.py
index d1a4695bc8..7c9054e250 100644
--- a/tests/test_completion/test_completion_install.py
+++ b/tests/test_completion/test_completion_install.py
@@ -133,11 +133,6 @@ def test_completion_install_fish():
assert "Completion will take effect once you restart the terminal" in result.stdout
-runner = CliRunner()
-app = typer.Typer()
-app.command()(mod.main)
-
-
def test_completion_install_powershell():
completion_path: Path = (
Path.home() / ".config/powershell/Microsoft.PowerShell_profile.ps1"
diff --git a/tests/test_rich_markup_mode.py b/tests/test_rich_markup_mode.py
new file mode 100644
index 0000000000..d9fe5bae4b
--- /dev/null
+++ b/tests/test_rich_markup_mode.py
@@ -0,0 +1,40 @@
+import typer
+import typer.completion
+from typer.testing import CliRunner
+
+runner = CliRunner()
+rounded = ["╭", "─", "┬", "╮", "│", "├", "┼", "┤", "╰", "┴", "╯"]
+
+
+def test_rich_markup_mode_none():
+ app = typer.Typer(rich_markup_mode=None)
+
+ @app.command()
+ def main(arg: str):
+ """Main function"""
+ print(f"Hello {arg}")
+
+ assert app.rich_markup_mode is None
+
+ result = runner.invoke(app, ["World"])
+ assert "Hello World" in result.stdout
+
+ result = runner.invoke(app, ["--help"])
+ assert all(c not in result.stdout for c in rounded)
+
+
+def test_rich_markup_mode_rich():
+ app = typer.Typer(rich_markup_mode="rich")
+
+ @app.command()
+ def main(arg: str):
+ """Main function"""
+ print(f"Hello {arg}")
+
+ assert app.rich_markup_mode == "rich"
+
+ result = runner.invoke(app, ["World"])
+ assert "Hello World" in result.stdout
+
+ result = runner.invoke(app, ["--help"])
+ assert any(c in result.stdout for c in rounded)
diff --git a/tests/test_tutorial/test_commands/test_index/test_tutorial004.py b/tests/test_tutorial/test_commands/test_index/test_tutorial004.py
new file mode 100644
index 0000000000..ae0139e93a
--- /dev/null
+++ b/tests/test_tutorial/test_commands/test_index/test_tutorial004.py
@@ -0,0 +1,45 @@
+import subprocess
+import sys
+
+from typer.testing import CliRunner
+
+from docs_src.commands.index import tutorial004 as mod
+
+app = mod.app
+
+runner = CliRunner()
+
+
+def test_help():
+ result = runner.invoke(app, ["--help"])
+ assert result.exit_code == 0
+ assert "[OPTIONS] COMMAND [ARGS]..." in result.output
+ print(result.output)
+ assert "Commands" in result.output
+ assert "create" in result.output
+ assert "delete" in result.output
+ # Test that the 'delete' command precedes the 'create' command in the help output
+ create_char = result.output.index("create")
+ delete_char = result.output.index("delete")
+ assert delete_char < create_char
+
+
+def test_create():
+ result = runner.invoke(app, ["create"])
+ assert result.exit_code == 0
+ assert "Creating user: Hiro Hamada" in result.output
+
+
+def test_delete():
+ result = runner.invoke(app, ["delete"])
+ assert result.exit_code == 0
+ assert "Deleting user: Hiro Hamada" in result.output
+
+
+def test_script():
+ result = subprocess.run(
+ [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "Usage" 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 ebd1e066ba..60304e9e55 100644
--- a/tests/test_tutorial/test_options_autocompletion/test_tutorial003.py
+++ b/tests/test_tutorial/test_options_autocompletion/test_tutorial003.py
@@ -9,7 +9,7 @@
runner = CliRunner()
-def test_completion():
+def test_completion_zsh():
result = subprocess.run(
[sys.executable, "-m", "coverage", "run", mod.__file__, " "],
capture_output=True,
@@ -25,6 +25,23 @@ def test_completion():
assert "Sebastian" in result.stdout
+def test_completion_powershell():
+ result = subprocess.run(
+ [sys.executable, "-m", "coverage", "run", mod.__file__, " "],
+ capture_output=True,
+ encoding="utf-8",
+ env={
+ **os.environ,
+ "_TUTORIAL003.PY_COMPLETE": "complete_powershell",
+ "_TYPER_COMPLETE_ARGS": "tutorial003.py --name Seb",
+ "_TYPER_COMPLETE_WORD_TO_COMPLETE": "Seb",
+ },
+ )
+ assert "Camila" not in result.stdout
+ assert "Carlos" not in result.stdout
+ assert "Sebastian" in result.stdout
+
+
def test_1():
result = runner.invoke(mod.app, ["--name", "Camila"])
assert result.exit_code == 0
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 8f12583e80..7688f108f5 100644
--- a/tests/test_tutorial/test_options_autocompletion/test_tutorial003_an.py
+++ b/tests/test_tutorial/test_options_autocompletion/test_tutorial003_an.py
@@ -9,7 +9,7 @@
runner = CliRunner()
-def test_completion():
+def test_completion_zsh():
result = subprocess.run(
[sys.executable, "-m", "coverage", "run", mod.__file__, " "],
capture_output=True,
@@ -25,6 +25,23 @@ def test_completion():
assert "Sebastian" in result.stdout
+def test_completion_powershell():
+ result = subprocess.run(
+ [sys.executable, "-m", "coverage", "run", mod.__file__, " "],
+ capture_output=True,
+ encoding="utf-8",
+ env={
+ **os.environ,
+ "_TUTORIAL003_AN.PY_COMPLETE": "complete_powershell",
+ "_TYPER_COMPLETE_ARGS": "tutorial003.py --name Seb",
+ "_TYPER_COMPLETE_WORD_TO_COMPLETE": "Seb",
+ },
+ )
+ assert "Camila" not in result.stdout
+ assert "Carlos" not in result.stdout
+ assert "Sebastian" in result.stdout
+
+
def test_1():
result = runner.invoke(mod.app, ["--name", "Camila"])
assert result.exit_code == 0
diff --git a/tests/utils.py b/tests/utils.py
index 9b503ff799..8f51332ee2 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -2,6 +2,18 @@
import pytest
+try:
+ import shellingham
+ from shellingham import ShellDetectionFailure
+
+ shell = shellingham.detect_shell()[0]
+except ImportError: # pragma: no cover
+ shellingham = None
+ shell = None
+except ShellDetectionFailure: # pragma: no cover
+ shell = None
+
+
needs_py310 = pytest.mark.skipif(
sys.version_info < (3, 10), reason="requires python3.10+"
)
@@ -9,3 +21,7 @@
needs_linux = pytest.mark.skipif(
not sys.platform.startswith("linux"), reason="Test requires Linux"
)
+
+needs_bash = pytest.mark.skipif(
+ not shellingham or not shell or "bash" not in shell, reason="Test requires Bash"
+)
diff --git a/typer/__init__.py b/typer/__init__.py
index dd2531263d..b422dd00d3 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.12.4"
+__version__ = "0.12.5"
from shutil import get_terminal_size as get_terminal_size
diff --git a/typer/_completion_classes.py b/typer/_completion_classes.py
index 6bb25c13bd..71ba031860 100644
--- a/typer/_completion_classes.py
+++ b/typer/_completion_classes.py
@@ -175,7 +175,7 @@ def get_completion_args(self) -> Tuple[List[str], str]:
completion_args = os.getenv("_TYPER_COMPLETE_ARGS", "")
incomplete = os.getenv("_TYPER_COMPLETE_WORD_TO_COMPLETE", "")
cwords = click.parser.split_arg_string(completion_args)
- args = cwords[1:]
+ args = cwords[1:-1] if incomplete else cwords[1:]
return args, incomplete
def format_completion(self, item: click.shell_completion.CompletionItem) -> str:
diff --git a/typer/_completion_shared.py b/typer/_completion_shared.py
index a5ff7b196a..4f40c143c0 100644
--- a/typer/_completion_shared.py
+++ b/typer/_completion_shared.py
@@ -100,13 +100,13 @@ def install_bash(*, prog_name: str, complete_var: str, shell: str) -> Path:
# It seems bash-completion is the official completion system for bash:
# Ref: https://www.gnu.org/software/bash/manual/html_node/A-Programmable-Completion-Example.html
# But installing in the locations from the docs doesn't seem to have effect
- completion_path = Path.home() / f".bash_completions/{prog_name}.sh"
+ completion_path = Path.home() / ".bash_completions" / f"{prog_name}.sh"
rc_path = Path.home() / ".bashrc"
rc_path.parent.mkdir(parents=True, exist_ok=True)
rc_content = ""
if rc_path.is_file():
rc_content = rc_path.read_text()
- completion_init_lines = [f"source {completion_path}"]
+ completion_init_lines = [f"source '{completion_path}'"]
for line in completion_init_lines:
if line not in rc_content: # pragma: no cover
rc_content += f"\n{line}"
diff --git a/typer/_typing.py b/typer/_typing.py
index b1d2483da0..147a50540b 100644
--- a/typer/_typing.py
+++ b/typer/_typing.py
@@ -1,347 +1,39 @@
# Copied from pydantic 1.9.2 (the latest version to support python 3.6.)
# https://github.com/pydantic/pydantic/blob/v1.9.2/pydantic/typing.py
+# Reduced drastically to only include Typer-specific 3.7+ functionality
# mypy: ignore-errors
import sys
-from os import PathLike
from typing import (
- TYPE_CHECKING,
- AbstractSet,
Any,
- ClassVar,
- Dict,
- Generator,
- List,
- Mapping,
- NewType,
+ Callable,
Optional,
- Sequence,
- Set,
Tuple,
Type,
Union,
- _eval_type,
- cast,
- get_type_hints,
)
-from typing_extensions import Annotated, Literal
-
-try:
- from typing import _TypingBase as typing_base # type: ignore
-except ImportError:
- from typing import _Final as typing_base # type: ignore
-
-try:
- from typing import GenericAlias as TypingGenericAlias # type: ignore
-except ImportError:
- # python < 3.9 does not have GenericAlias (list[int], tuple[str, ...] and so on)
- TypingGenericAlias = ()
-
-try:
- from types import UnionType as TypesUnionType # type: ignore
-except ImportError:
- # python < 3.10 does not have UnionType (str | int, byte | bool and so on)
- TypesUnionType = ()
-
-
-if sys.version_info < (3, 7):
- if TYPE_CHECKING:
-
- class ForwardRef:
- def __init__(self, arg: Any):
- pass
-
- def _eval_type(self, globalns: Any, localns: Any) -> Any:
- pass
-
- else:
- from typing import _ForwardRef as ForwardRef
-else:
- from typing import ForwardRef
-
-
-if sys.version_info < (3, 7):
-
- def evaluate_forwardref(type_: ForwardRef, globalns: Any, localns: Any) -> Any:
- return type_._eval_type(globalns, localns)
-
-elif sys.version_info < (3, 9):
-
- def evaluate_forwardref(type_: ForwardRef, globalns: Any, localns: Any) -> Any:
- return type_._evaluate(globalns, localns)
-
-else:
-
- def evaluate_forwardref(type_: ForwardRef, globalns: Any, localns: Any) -> Any:
- # Even though it is the right signature for python 3.9, mypy complains with
- # `error: Too many arguments for "_evaluate" of "ForwardRef"` hence the cast...
- return cast(Any, type_)._evaluate(globalns, localns, set())
-
-
-if sys.version_info < (3, 9):
- # Ensure we always get all the whole `Annotated` hint, not just the annotated type.
- # For 3.6 to 3.8, `get_type_hints` doesn't recognize `typing_extensions.Annotated`,
- # so it already returns the full annotation
- get_all_type_hints = get_type_hints
-
-else:
-
- def get_all_type_hints(obj: Any, globalns: Any = None, localns: Any = None) -> Any:
- return get_type_hints(obj, globalns, localns, include_extras=True)
-
-
-if sys.version_info < (3, 7):
- from typing import Callable as Callable
-
- AnyCallable = Callable[..., Any]
- NoArgAnyCallable = Callable[[], Any]
-else:
- from collections.abc import Callable as Callable
- from typing import Callable as TypingCallable
-
- AnyCallable = TypingCallable[..., Any]
- NoArgAnyCallable = TypingCallable[[], Any]
-
-
-# Annotated[...] is implemented by returning an instance of one of these classes, depending on
-# python/typing_extensions version.
-AnnotatedTypeNames = {"AnnotatedMeta", "_AnnotatedAlias"}
-
-
-if sys.version_info < (3, 8):
-
- def get_origin(t: Type[Any]) -> Optional[Type[Any]]:
- if type(t).__name__ in AnnotatedTypeNames:
- return cast(
- Type[Any], Annotated
- ) # mypy complains about _SpecialForm in py3.6
- return getattr(t, "__origin__", None)
-
-else:
- from typing import get_origin as _typing_get_origin
-
- def get_origin(tp: Type[Any]) -> Optional[Type[Any]]:
- """
- We can't directly use `typing.get_origin` since we need a fallback to support
- custom generic classes like `ConstrainedList`
- It should be useless once https://github.com/cython/cython/issues/3537 is
- solved and https://github.com/samuelcolvin/pydantic/pull/1753 is merged.
- """
- if type(tp).__name__ in AnnotatedTypeNames:
- return cast(Type[Any], Annotated) # mypy complains about _SpecialForm
- return _typing_get_origin(tp) or getattr(tp, "__origin__", None)
-
-
-if sys.version_info < (3, 7): # noqa: C901 (ignore complexity)
-
- def get_args(t: Type[Any]) -> Tuple[Any, ...]:
- """Simplest get_args compatibility layer possible.
-
- The Python 3.6 typing module does not have `_GenericAlias` so
- this won't work for everything. In particular this will not
- support the `generics` module (we don't support generic models in
- python 3.6).
-
- """
- if type(t).__name__ in AnnotatedTypeNames:
- return t.__args__ + t.__metadata__
- return getattr(t, "__args__", ())
-
-elif sys.version_info < (3, 8): # noqa: C901
- from typing import _GenericAlias
-
- def get_args(t: Type[Any]) -> Tuple[Any, ...]:
- """Compatibility version of get_args for python 3.7.
-
- Mostly compatible with the python 3.8 `typing` module version
- and able to handle almost all use cases.
- """
- if type(t).__name__ in AnnotatedTypeNames:
- return t.__args__ + t.__metadata__
- if isinstance(t, _GenericAlias):
- res = t.__args__
- if t.__origin__ is Callable and res and res[0] is not Ellipsis:
- res = (list(res[:-1]), res[-1])
- return res
- return getattr(t, "__args__", ())
-
-else:
- from typing import get_args as _typing_get_args
-
- def _generic_get_args(tp: Type[Any]) -> Tuple[Any, ...]:
- """
- In python 3.9, `typing.Dict`, `typing.List`, ...
- do have an empty `__args__` by default (instead of the generic ~T for example).
- In order to still support `Dict` for example and consider it as `Dict[Any, Any]`,
- we retrieve the `_nparams` value that tells us how many parameters it needs.
- """
- if hasattr(tp, "_nparams"):
- return (Any,) * tp._nparams
- return ()
-
- def get_args(tp: Type[Any]) -> Tuple[Any, ...]:
- """Get type arguments with all substitutions performed.
-
- For unions, basic simplifications used by Union constructor are performed.
- Examples::
- get_args(Dict[str, int]) == (str, int)
- get_args(int) == ()
- get_args(Union[int, Union[T, int], str][int]) == (int, str)
- get_args(Union[int, Tuple[T, int]][str]) == (int, Tuple[str, int])
- get_args(Callable[[], T][int]) == ([], int)
- """
- if type(tp).__name__ in AnnotatedTypeNames:
- return tp.__args__ + tp.__metadata__
- # the fallback is needed for the same reasons as `get_origin` (see above)
- return (
- _typing_get_args(tp) or getattr(tp, "__args__", ()) or _generic_get_args(tp)
- )
-
-
-if sys.version_info < (3, 9):
-
- def convert_generics(tp: Type[Any]) -> Type[Any]:
- """Python 3.9 and older only supports generics from `typing` module.
- They convert strings to ForwardRef automatically.
-
- Examples::
- typing.List['Hero'] == typing.List[ForwardRef('Hero')]
- """
- return tp
-
-else:
- from typing import _UnionGenericAlias # type: ignore
-
- from typing_extensions import _AnnotatedAlias
-
- def convert_generics(tp: Type[Any]) -> Type[Any]:
- """
- Recursively searches for `str` type hints and replaces them with ForwardRef.
-
- Examples::
- convert_generics(list['Hero']) == list[ForwardRef('Hero')]
- convert_generics(dict['Hero', 'Team']) == dict[ForwardRef('Hero'), ForwardRef('Team')]
- convert_generics(typing.Dict['Hero', 'Team']) == typing.Dict[ForwardRef('Hero'), ForwardRef('Team')]
- convert_generics(list[str | 'Hero'] | int) == list[str | ForwardRef('Hero')] | int
- """
- origin = get_origin(tp)
- if not origin or not hasattr(tp, "__args__"):
- return tp
-
- args = get_args(tp)
-
- # typing.Annotated needs special treatment
- if origin is Annotated:
- return _AnnotatedAlias(convert_generics(args[0]), args[1:])
-
- # recursively replace `str` instances inside of `GenericAlias` with `ForwardRef(arg)`
- converted = tuple(
- ForwardRef(arg)
- if isinstance(arg, str) and isinstance(tp, TypingGenericAlias)
- else convert_generics(arg)
- for arg in args
- )
-
- if converted == args:
- return tp
- elif isinstance(tp, TypingGenericAlias):
- return TypingGenericAlias(origin, converted)
- elif isinstance(tp, TypesUnionType):
- # recreate types.UnionType (PEP604, Python >= 3.10)
- return _UnionGenericAlias(origin, converted)
- else:
- try:
- setattr(tp, "__args__", converted) # noqa: B010
- except AttributeError:
- pass
- return tp
-
+from typing_extensions import Literal, get_args, get_origin
if sys.version_info < (3, 10):
def is_union(tp: Optional[Type[Any]]) -> bool:
return tp is Union
- WithArgsTypes = (TypingGenericAlias,)
-
else:
import types
- import typing
def is_union(tp: Optional[Type[Any]]) -> bool:
return tp is Union or tp is types.UnionType # noqa: E721
- WithArgsTypes = (typing._GenericAlias, types.GenericAlias, types.UnionType)
-
-
-if sys.version_info < (3, 9):
- StrPath = Union[str, PathLike]
-else:
- StrPath = Union[str, PathLike]
- # TODO: Once we switch to Cython 3 to handle generics properly
- # (https://github.com/cython/cython/issues/2753), use following lines instead
- # of the one above
- # # os.PathLike only becomes subscriptable from Python 3.9 onwards
- # StrPath = Union[str, PathLike[str]]
-
-
-if TYPE_CHECKING:
- # Only in Pydantic
- # from .fields import ModelField
-
- TupleGenerator = Generator[Tuple[str, Any], None, None]
- DictStrAny = Dict[str, Any]
- DictAny = Dict[Any, Any]
- SetStr = Set[str]
- ListStr = List[str]
- IntStr = Union[int, str]
- AbstractSetIntStr = AbstractSet[IntStr]
- DictIntStrAny = Dict[IntStr, Any]
- MappingIntStrAny = Mapping[IntStr, Any]
- CallableGenerator = Generator[AnyCallable, None, None]
- ReprArgs = Sequence[Tuple[Optional[str], Any]]
- AnyClassMethod = classmethod[Any]
__all__ = (
- "ForwardRef",
- "Callable",
- "AnyCallable",
- "NoArgAnyCallable",
"NoneType",
"is_none_type",
- "display_as_type",
- "resolve_annotations",
"is_callable_type",
"is_literal_type",
"all_literal_values",
- "is_namedtuple",
- "is_typeddict",
- "is_new_type",
- "new_type_supertype",
- "is_classvar",
- "update_field_forward_refs",
- "update_model_forward_refs",
- "TupleGenerator",
- "DictStrAny",
- "DictAny",
- "SetStr",
- "ListStr",
- "IntStr",
- "AbstractSetIntStr",
- "DictIntStrAny",
- "CallableGenerator",
- "ReprArgs",
- "AnyClassMethod",
- "CallableGenerator",
- "WithArgsTypes",
- "get_args",
- "get_origin",
- "get_sub_types",
- "typing_base",
- "get_all_type_hints",
"is_union",
- "StrPath",
)
@@ -352,8 +44,8 @@ def is_union(tp: Optional[Type[Any]]) -> bool:
if sys.version_info < (3, 8):
- # Even though this implementation is slower, we need it for python 3.6/3.7:
- # In python 3.6/3.7 "Literal" is not a builtin type and uses a different
+ # Even though this implementation is slower, we need it for python 3.7:
+ # In python 3.7 "Literal" is not a builtin type and uses a different
# mechanism.
# for this reason `Literal[None] is Literal[None]` evaluates to `False`,
# breaking the faster implementation used for the other python versions.
@@ -382,91 +74,16 @@ def is_none_type(type_: Any) -> bool:
return False
-def display_as_type(v: Type[Any]) -> str:
- if (
- not isinstance(v, typing_base)
- and not isinstance(v, WithArgsTypes)
- and not isinstance(v, type)
- ):
- v = v.__class__
-
- if is_union(get_origin(v)):
- return f'Union[{", ".join(map(display_as_type, get_args(v)))}]'
-
- if isinstance(v, WithArgsTypes):
- # Generic alias are constructs like `list[int]`
- return str(v).replace("typing.", "")
-
- try:
- return v.__name__
- except AttributeError:
- # happens with typing objects
- return str(v).replace("typing.", "")
-
-
-def resolve_annotations(
- raw_annotations: Dict[str, Type[Any]], module_name: Optional[str]
-) -> Dict[str, Type[Any]]:
- """
- Partially taken from typing.get_type_hints.
-
- Resolve string or ForwardRef annotations into type objects if possible.
- """
- base_globals: Optional[Dict[str, Any]] = None
- if module_name:
- try:
- module = sys.modules[module_name]
- except KeyError:
- # happens occasionally, see https://github.com/samuelcolvin/pydantic/issues/2363
- pass
- else:
- base_globals = module.__dict__
-
- annotations = {}
- for name, value in raw_annotations.items():
- if isinstance(value, str):
- if (3, 10) > sys.version_info >= (3, 9, 8) or sys.version_info >= (
- 3,
- 10,
- 1,
- ):
- value = ForwardRef(value, is_argument=False, is_class=True)
- elif sys.version_info >= (3, 7):
- value = ForwardRef(value, is_argument=False)
- else:
- value = ForwardRef(value)
- try:
- value = _eval_type(value, base_globals, None)
- except NameError:
- # this is ok, it can be fixed with update_forward_refs
- pass
- annotations[name] = value
- return annotations
-
-
def is_callable_type(type_: Type[Any]) -> bool:
return type_ is Callable or get_origin(type_) is Callable
-if sys.version_info >= (3, 7):
+def is_literal_type(type_: Type[Any]) -> bool:
+ return Literal is not None and get_origin(type_) is Literal
- def is_literal_type(type_: Type[Any]) -> bool:
- return Literal is not None and get_origin(type_) is Literal
- def literal_values(type_: Type[Any]) -> Tuple[Any, ...]:
- return get_args(type_)
-
-else:
-
- def is_literal_type(type_: Type[Any]) -> bool:
- return (
- Literal is not None
- and hasattr(type_, "__values__")
- and type_ == Literal[type_.__values__]
- )
-
- def literal_values(type_: Type[Any]) -> Tuple[Any, ...]:
- return type_.__values__
+def literal_values(type_: Type[Any]) -> Tuple[Any, ...]:
+ return get_args(type_)
def all_literal_values(type_: Type[Any]) -> Tuple[Any, ...]:
@@ -480,151 +97,3 @@ def all_literal_values(type_: Type[Any]) -> Tuple[Any, ...]:
values = literal_values(type_)
return tuple(x for value in values for x in all_literal_values(value))
-
-
-def is_namedtuple(type_: Type[Any]) -> bool:
- """
- Check if a given class is a named tuple.
- It can be either a `typing.NamedTuple` or `collections.namedtuple`
- """
- from .utils import lenient_issubclass
-
- return lenient_issubclass(type_, tuple) and hasattr(type_, "_fields")
-
-
-def is_typeddict(type_: Type[Any]) -> bool:
- """
- Check if a given class is a typed dict (from `typing` or `typing_extensions`)
- In 3.10, there will be a public method (https://docs.python.org/3.10/library/typing.html#typing.is_typeddict)
- """
- from .utils import lenient_issubclass
-
- return lenient_issubclass(type_, dict) and hasattr(type_, "__total__")
-
-
-test_type = NewType("test_type", str)
-
-
-def is_new_type(type_: Type[Any]) -> bool:
- """
- Check whether type_ was created using typing.NewType
- """
- return isinstance(type_, test_type.__class__) and hasattr(type_, "__supertype__") # type: ignore
-
-
-def new_type_supertype(type_: Type[Any]) -> Type[Any]:
- while hasattr(type_, "__supertype__"):
- type_ = type_.__supertype__
- return type_
-
-
-def _check_classvar(v: Optional[Type[Any]]) -> bool:
- if v is None:
- return False
-
- return v.__class__ == ClassVar.__class__ and (
- sys.version_info < (3, 7) or getattr(v, "_name", None) == "ClassVar"
- )
-
-
-def is_classvar(ann_type: Type[Any]) -> bool:
- if _check_classvar(ann_type) or _check_classvar(get_origin(ann_type)):
- return True
-
- # this is an ugly workaround for class vars that contain forward references and are therefore themselves
- # forward references, see #3679
- if ann_type.__class__ == ForwardRef and ann_type.__forward_arg__.startswith(
- "ClassVar["
- ):
- return True
-
- return False
-
-
-# Only in Pydantic
-# def update_field_forward_refs(field: "ModelField", globalns: Any, localns: Any) -> None:
-# """
-# Try to update ForwardRefs on fields based on this ModelField, globalns and localns.
-# """
-# if field.type_.__class__ == ForwardRef:
-# field.type_ = evaluate_forwardref(field.type_, globalns, localns or None)
-# field.prepare()
-
-# if field.sub_fields:
-# for sub_f in field.sub_fields:
-# update_field_forward_refs(sub_f, globalns=globalns, localns=localns)
-
-# if field.discriminator_key is not None:
-# field.prepare_discriminated_union_sub_fields()
-
-
-# Only in Pydantic
-# def update_model_forward_refs(
-# model: Type[Any],
-# fields: Iterable["ModelField"],
-# json_encoders: Dict[Union[Type[Any], str], AnyCallable],
-# localns: "DictStrAny",
-# exc_to_suppress: Tuple[Type[BaseException], ...] = (),
-# ) -> None:
-# """
-# Try to update model fields ForwardRefs based on model and localns.
-# """
-# if model.__module__ in sys.modules:
-# globalns = sys.modules[model.__module__].__dict__.copy()
-# else:
-# globalns = {}
-
-# globalns.setdefault(model.__name__, model)
-
-# for f in fields:
-# try:
-# update_field_forward_refs(f, globalns=globalns, localns=localns)
-# except exc_to_suppress:
-# pass
-
-# for key in set(json_encoders.keys()):
-# if isinstance(key, str):
-# fr: ForwardRef = ForwardRef(key)
-# elif isinstance(key, ForwardRef):
-# fr = key
-# else:
-# continue
-
-# try:
-# new_key = evaluate_forwardref(fr, globalns, localns or None)
-# except exc_to_suppress: # pragma: no cover
-# continue
-
-# json_encoders[new_key] = json_encoders.pop(key)
-
-
-def get_class(type_: Type[Any]) -> Union[None, bool, Type[Any]]:
- """
- Tries to get the class of a Type[T] annotation. Returns True if Type is used
- without brackets. Otherwise returns None.
- """
- try:
- origin = get_origin(type_)
- if origin is None: # Python 3.6
- origin = type_
- if issubclass(origin, Type): # type: ignore
- if not get_args(type_) or not isinstance(get_args(type_)[0], type):
- return True
- return get_args(type_)[0]
- except (AttributeError, TypeError):
- pass
- return None
-
-
-def get_sub_types(tp: Any) -> List[Any]:
- """
- Return all the types that are allowed by type `tp`
- `tp` can be a `Union` of allowed types or an `Annotated` type
- """
- origin = get_origin(tp)
- if origin is Annotated:
- return get_sub_types(get_args(tp)[0])
- elif is_union(origin):
- return [x for t in get_args(tp) for x in get_sub_types(t)]
- else:
- return [tp]
diff --git a/typer/core.py b/typer/core.py
index 31fece5a76..00e21da869 100644
--- a/typer/core.py
+++ b/typer/core.py
@@ -31,15 +31,18 @@
else:
from typing_extensions import Literal
+MarkupMode = Literal["markdown", "rich", None]
+
try:
import rich
from . import rich_utils
+ DEFAULT_MARKUP_MODE: MarkupMode = "rich"
+
except ImportError: # pragma: no cover
rich = None # type: ignore
-
-MarkupMode = Literal["markdown", "rich", None]
+ DEFAULT_MARKUP_MODE = None
# Copy from click.parser._split_opt
@@ -167,6 +170,7 @@ def _main(
complete_var: Optional[str] = None,
standalone_mode: bool = True,
windows_expand_args: bool = True,
+ rich_markup_mode: MarkupMode = DEFAULT_MARKUP_MODE,
**extra: Any,
) -> Any:
# Typer override, duplicated from click.main() to handle custom rich exceptions
@@ -208,7 +212,7 @@ def _main(
if not standalone_mode:
raise
# Typer override
- if rich:
+ if rich and rich_markup_mode is not None:
rich_utils.rich_format_error(e)
else:
e.show()
@@ -238,7 +242,7 @@ def _main(
if not standalone_mode:
raise
# Typer override
- if rich:
+ if rich and rich_markup_mode is not None:
rich_utils.rich_abort_error()
else:
click.echo(_("Aborted!"), file=sys.stderr)
@@ -614,7 +618,7 @@ def __init__(
hidden: bool = False,
deprecated: bool = False,
# Rich settings
- rich_markup_mode: MarkupMode = None,
+ rich_markup_mode: MarkupMode = DEFAULT_MARKUP_MODE,
rich_help_panel: Union[str, None] = None,
) -> None:
super().__init__(
@@ -665,11 +669,12 @@ def main(
complete_var=complete_var,
standalone_mode=standalone_mode,
windows_expand_args=windows_expand_args,
+ rich_markup_mode=self.rich_markup_mode,
**extra,
)
def format_help(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
- if not rich:
+ if not rich or self.rich_markup_mode is None:
return super().format_help(ctx, formatter)
return rich_utils.rich_format_help(
obj=self,
@@ -687,7 +692,7 @@ def __init__(
Union[Dict[str, click.Command], Sequence[click.Command]]
] = None,
# Rich settings
- rich_markup_mode: MarkupMode = None,
+ rich_markup_mode: MarkupMode = DEFAULT_MARKUP_MODE,
rich_help_panel: Union[str, None] = None,
**attrs: Any,
) -> None:
@@ -727,14 +732,21 @@ def main(
complete_var=complete_var,
standalone_mode=standalone_mode,
windows_expand_args=windows_expand_args,
+ rich_markup_mode=self.rich_markup_mode,
**extra,
)
def format_help(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
- if not rich:
+ if not rich or self.rich_markup_mode is None:
return super().format_help(ctx, formatter)
return rich_utils.rich_format_help(
obj=self,
ctx=ctx,
markup_mode=self.rich_markup_mode,
)
+
+ def list_commands(self, ctx: click.Context) -> List[str]:
+ """Returns a list of subcommand names.
+ Note that in Click's Group class, these are sorted.
+ In Typer, we wish to maintain the original order of creation (cf Issue #933)"""
+ return [n for n, c in self.commands.items()]
diff --git a/typer/main.py b/typer/main.py
index 30bfa41fd0..e8440d8ca9 100644
--- a/typer/main.py
+++ b/typer/main.py
@@ -12,10 +12,18 @@
from uuid import UUID
import click
+from typing_extensions import get_args, get_origin
-from ._typing import get_args, get_origin, is_union
+from ._typing import is_union
from .completion import get_completion_inspect_parameters
-from .core import MarkupMode, TyperArgument, TyperCommand, TyperGroup, TyperOption
+from .core import (
+ DEFAULT_MARKUP_MODE,
+ MarkupMode,
+ TyperArgument,
+ TyperCommand,
+ TyperGroup,
+ TyperOption,
+)
from .models import (
AnyType,
ArgumentInfo,
@@ -39,10 +47,11 @@
try:
import rich
- from rich.console import Console
from rich.traceback import Traceback
- console_stderr = Console(stderr=True)
+ from . import rich_utils
+
+ console_stderr = rich_utils._get_rich_console(stderr=True)
except ImportError: # pragma: no cover
rich = None # type: ignore
@@ -70,12 +79,15 @@ def except_hook(
supress_internal_dir_names = [typer_path, click_path]
exc = exc_value
if rich:
+ from .rich_utils import MAX_WIDTH
+
rich_tb = Traceback.from_exception(
type(exc),
exc,
exc.__traceback__,
show_locals=exception_config.pretty_exceptions_show_locals,
suppress=supress_internal_dir_names,
+ width=MAX_WIDTH,
)
console_stderr.print(rich_tb)
return
@@ -133,7 +145,7 @@ def __init__(
deprecated: bool = Default(False),
add_completion: bool = True,
# Rich settings
- rich_markup_mode: MarkupMode = None,
+ rich_markup_mode: MarkupMode = Default(DEFAULT_MARKUP_MODE),
rich_help_panel: Union[str, None] = Default(None),
pretty_exceptions_enable: bool = True,
pretty_exceptions_show_locals: bool = True,
@@ -436,7 +448,6 @@ def solve_typer_info_help(typer_info: TyperInfo) -> str:
def solve_typer_info_defaults(typer_info: TyperInfo) -> TyperInfo:
values: Dict[str, Any] = {}
- name = None
for name, value in typer_info.__dict__.items():
# Priority 1: Value was set in app.add_typer()
if not isinstance(value, DefaultPlaceholder):
@@ -505,7 +516,7 @@ def get_group_from_info(
context_param_name,
) = get_params_convertors_ctx_param_name_from_function(solved_info.callback)
cls = solved_info.cls or TyperGroup
- assert issubclass(cls, TyperGroup)
+ assert issubclass(cls, TyperGroup), f"{cls} should be a subclass of {TyperGroup}"
group = cls(
name=solved_info.name or "",
commands=commands,
@@ -717,9 +728,9 @@ def get_click_type(
elif parameter_info.parser is not None:
return click.types.FuncParamType(parameter_info.parser)
- elif annotation == str:
+ elif annotation is str:
return click.STRING
- elif annotation == int:
+ elif annotation is int:
if parameter_info.min is not None or parameter_info.max is not None:
min_ = None
max_ = None
@@ -730,7 +741,7 @@ def get_click_type(
return click.IntRange(min=min_, max=max_, clamp=parameter_info.clamp)
else:
return click.INT
- elif annotation == float:
+ elif annotation is float:
if parameter_info.min is not None or parameter_info.max is not None:
return click.FloatRange(
min=parameter_info.min,
@@ -739,7 +750,7 @@ def get_click_type(
)
else:
return click.FLOAT
- elif annotation == bool:
+ elif annotation is bool:
return click.BOOL
elif annotation == UUID:
return click.UUID
@@ -838,7 +849,7 @@ def get_click_param(
else:
default_value = param.default
parameter_info = OptionInfo()
- annotation: Any = Any
+ annotation: Any
if not param.annotation == param.empty:
annotation = param.annotation
else:
diff --git a/typer/params.py b/typer/params.py
index 176ace2b36..77c86b694e 100644
--- a/typer/params.py
+++ b/typer/params.py
@@ -69,8 +69,7 @@ def Option(
path_type: Union[None, Type[str], Type[bytes]] = None,
# Rich settings
rich_help_panel: Union[str, None] = None,
-) -> Any:
- ...
+) -> Any: ...
# Overload for Option created with custom type 'click_type'
@@ -133,8 +132,7 @@ def Option(
path_type: Union[None, Type[str], Type[bytes]] = None,
# Rich settings
rich_help_panel: Union[str, None] = None,
-) -> Any:
- ...
+) -> Any: ...
def Option(
@@ -307,8 +305,7 @@ def Argument(
path_type: Union[None, Type[str], Type[bytes]] = None,
# Rich settings
rich_help_panel: Union[str, None] = None,
-) -> Any:
- ...
+) -> Any: ...
# Overload for Argument created with custom type 'click_type'
@@ -363,8 +360,7 @@ def Argument(
path_type: Union[None, Type[str], Type[bytes]] = None,
# Rich settings
rich_help_panel: Union[str, None] = None,
-) -> Any:
- ...
+) -> Any: ...
def Argument(
diff --git a/typer/rich_utils.py b/typer/rich_utils.py
index cf0538e914..905c5a3dbf 100644
--- a/typer/rich_utils.py
+++ b/typer/rich_utils.py
@@ -68,9 +68,9 @@
STYLE_ABORTED = "red"
_TERMINAL_WIDTH = getenv("TERMINAL_WIDTH")
MAX_WIDTH = int(_TERMINAL_WIDTH) if _TERMINAL_WIDTH else None
-COLOR_SYSTEM: Optional[
- Literal["auto", "standard", "256", "truecolor", "windows"]
-] = "auto" # Set to None to disable colors
+COLOR_SYSTEM: Optional[Literal["auto", "standard", "256", "truecolor", "windows"]] = (
+ "auto" # Set to None to disable colors
+)
_TYPER_FORCE_DISABLE_TERMINAL = getenv("_TYPER_FORCE_DISABLE_TERMINAL")
FORCE_TERMINAL = (
True
@@ -144,7 +144,7 @@ def _get_rich_console(stderr: bool = False) -> Console:
)
-def _make_rich_rext(
+def _make_rich_text(
*, text: str, style: str = "", markup_mode: MarkupMode
) -> Union[Markdown, Text]:
"""Take a string, remove indentations, and return styled text.
@@ -194,7 +194,7 @@ def _get_help_text(
# Remove single linebreaks
if markup_mode != MARKUP_MODE_MARKDOWN and not first_line.startswith("\b"):
first_line = first_line.replace("\n", " ")
- yield _make_rich_rext(
+ yield _make_rich_text(
text=first_line.strip(),
style=STYLE_HELPTEXT_FIRST_LINE,
markup_mode=markup_mode,
@@ -217,7 +217,7 @@ def _get_help_text(
# Join with double linebreaks if markdown
remaining_lines = "\n\n".join(remaining_paragraphs)
- yield _make_rich_rext(
+ yield _make_rich_text(
text=remaining_lines,
style=STYLE_HELPTEXT,
markup_mode=markup_mode,
@@ -272,7 +272,7 @@ def _get_parameter_help(
for x in paragraphs
]
items.append(
- _make_rich_rext(
+ _make_rich_text(
text="\n".join(paragraphs).strip(),
style=STYLE_OPTION_HELP,
markup_mode=markup_mode,
@@ -331,7 +331,7 @@ def _make_command_help(
paragraphs[0] = paragraphs[0].replace("\n", " ")
elif paragraphs[0].startswith("\b"):
paragraphs[0] = paragraphs[0].replace("\b\n", "")
- return _make_rich_rext(
+ return _make_rich_text(
text=paragraphs[0].strip(),
style=STYLE_OPTION_HELP,
markup_mode=markup_mode,
@@ -674,7 +674,7 @@ def rich_format_help(
# Remove single linebreaks, replace double with single
lines = obj.epilog.split("\n\n")
epilogue = "\n".join([x.replace("\n", " ").strip() for x in lines])
- epilogue_text = _make_rich_rext(text=epilogue, markup_mode=markup_mode)
+ epilogue_text = _make_rich_text(text=epilogue, markup_mode=markup_mode)
console.print(Padding(Align(epilogue_text, pad=False), 1))
diff --git a/typer/utils.py b/typer/utils.py
index 5c0e967a64..93c407447e 100644
--- a/typer/utils.py
+++ b/typer/utils.py
@@ -3,9 +3,8 @@
from copy import copy
from typing import Any, Callable, Dict, List, Tuple, Type, cast
-from typing_extensions import Annotated, get_type_hints
+from typing_extensions import Annotated, get_args, get_origin, get_type_hints
-from ._typing import get_args, get_origin
from .models import ArgumentInfo, OptionInfo, ParameterInfo, ParamMeta
@@ -96,7 +95,7 @@ def __str__(self) -> str:
def _split_annotation_from_typer_annotations(
base_annotation: Type[Any],
) -> Tuple[Type[Any], List[ParameterInfo]]:
- if get_origin(base_annotation) is not Annotated: # type: ignore
+ if get_origin(base_annotation) is not Annotated:
return base_annotation, []
base_annotation, *maybe_typer_annotations = get_args(base_annotation)
return base_annotation, [