Skip to content

Commit 2d7be49

Browse files
committed
Use typer for command-line interface creation
1 parent 6729b3d commit 2d7be49

File tree

1 file changed

+61
-91
lines changed

1 file changed

+61
-91
lines changed

source/guides/creating-command-line-tools.rst

Lines changed: 61 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -35,97 +35,64 @@ named after the main module:
3535

3636
.. code-block:: python
3737
38-
def greet(name="", gender="", knight=False, count=1):
39-
greeting = "Greetings, dear "
40-
masculine = gender == "masculine"
41-
feminine = gender == "feminine"
42-
if gender or knight:
43-
salutation = ""
44-
if knight:
45-
salutation = "Sir "
46-
elif masculine:
47-
salutation = "Mr. "
48-
elif feminine:
49-
salutation = "Ms. "
50-
greeting += salutation
51-
if name:
52-
greeting += f"{name}!"
53-
else:
54-
pronoun = "her" if feminine else "his" if masculine or knight else "its"
55-
greeting += f"what's-{pronoun}-name!"
56-
else:
57-
if name:
58-
greeting += f"{name}!"
59-
elif not gender:
60-
greeting += "friend!"
61-
for i in range(0, count):
62-
print(greeting)
38+
import typer
39+
from typing_extensions import Annotated
40+
41+
42+
def greet(
43+
name: Annotated[str, typer.Argument(help="The (last, if --gender is given) name of the person to greet")] = "",
44+
gender: Annotated[str, typer.Option(help="The gender of the person to greet")] = "",
45+
knight: Annotated[bool, typer.Option(help="Whether the person is a knight")] = False,
46+
count: Annotated[int, typer.Option(help="Number of times to greet the person")] = 1
47+
):
48+
greeting = "Greetings, dear "
49+
masculine = gender == "masculine"
50+
feminine = gender == "feminine"
51+
if gender or knight:
52+
salutation = ""
53+
if knight:
54+
salutation = "Sir "
55+
elif masculine:
56+
salutation = "Mr. "
57+
elif feminine:
58+
salutation = "Ms. "
59+
greeting += salutation
60+
if name:
61+
greeting += f"{name}!"
62+
else:
63+
pronoun = "her" if feminine else "his" if masculine or knight else "its"
64+
greeting += f"what's-{pronoun}-name"
65+
else:
66+
if name:
67+
greeting += f"{name}!"
68+
elif not gender:
69+
greeting += "friend!"
70+
for i in range(0, count):
71+
print(greeting)
6372
6473
The above function receives several keyword arguments that determine how the greeting to output is constructed.
6574
Now, construct the command-line interface to provision it with the same, which is done
6675
in :file:`cli.py`:
6776

6877
.. code-block:: python
6978
70-
import argparse
71-
import sys
72-
73-
from .greet import greet
74-
75-
_arg_spec = {
76-
'--name': {
77-
'metavar': 'STRING',
78-
'type': str,
79-
'help': 'The (last, if "gender" is given) name of the person to greet',
80-
},
81-
'--count': {
82-
'metavar': 'INT',
83-
'type': int,
84-
'default': 1,
85-
'help': 'Number of times to greet the person',
86-
},
87-
88-
}
89-
_arg_spec_mutually_exclusive = {
90-
'--gender': {
91-
'metavar': 'STRING',
92-
'type': str,
93-
'help': 'The gender of the person to greet',
94-
},
95-
'--knight': {
96-
'action': 'store_true',
97-
'default': False,
98-
'help': 'Whether the person is a knight',
99-
},
100-
}
101-
102-
103-
def main():
104-
parser = argparse.ArgumentParser(
105-
description="Greet a person (semi-)formally."
106-
)
107-
group = parser.add_mutually_exclusive_group()
108-
for arg, spec in _arg_spec.items():
109-
parser.add_argument(arg, **spec)
110-
for arg, spec in _arg_spec_mutually_exclusive.items():
111-
group.add_argument(arg, **spec)
112-
parsed_args = parser.parse_args()
113-
args = {
114-
arg: value
115-
for arg, value in vars(parsed_args).items()
116-
if value is not None
117-
}
118-
# Run the function with the command-line arguments as keyword arguments.
119-
# A more complex setup is normally initialized at this point.
120-
greet(**args)
79+
import typer
12180
81+
from .hello import greet
82+
83+
84+
app = typer.Typer()
85+
app.command()(greet)
12286
123-
if __name__ == "__main__":
124-
sys.exit(main())
12587
126-
The command-line interface is built with :py:mod:`argparse`, a command-line parser which is included in Python's
127-
standard library. It is a bit rudimentary but sufficient for most needs. Another easy-to-use alternative is docopt_;
128-
advanced users are encouraged to make use of click_ or typer_.
88+
if __name__ == "__main__":
89+
app()
90+
91+
The command-line interface is built with typer_, an easy-to-use CLI parser based on Python type hints. It provides
92+
auto-completion and nicely styled command-line help out of the box. Another option would be :py:mod:`argparse`,
93+
a command-line parser which is included in Python's standard library. It is sufficient for most needs, but requires
94+
a lot of code, usually in ``cli.py``, to function properly. Alternatively, docopt_ makes it possible to create CLI
95+
interfaces based solely on docstrings; advanced users are encouraged to make use of click_ (on which ``typer`` is based).
12996

13097
Now, add an empty :file:`__init__.py` file, to define the project as a regular :term:`import package <Import Package>`.
13198

@@ -135,11 +102,9 @@ so initizalize the command-line interface here:
135102

136103
.. code-block:: python
137104
138-
import sys
139-
140105
if __name__ == "__main__":
141-
from greetings.cli import main
142-
sys.exit(main())
106+
from greetings.cli import app
107+
app()
143108
144109
.. note::
145110

@@ -151,14 +116,15 @@ so initizalize the command-line interface here:
151116
``pyproject.toml``
152117
------------------
153118

154-
The project's :term:`metadata <Pyproject Metadata>` is placed in :term:`pyproject.toml`. The :term:`pyproject metadata keys <Pyproject Metadata Key>` and the ``[build-system]`` table may be filled in as described in :ref:`writing-pyproject-toml`.
119+
The project's :term:`metadata <Pyproject Metadata>` is placed in :term:`pyproject.toml`. The :term:`pyproject metadata keys <Pyproject Metadata Key>` and the ``[build-system]`` table may be filled in as described in :ref:`writing-pyproject-toml`, adding a dependency
120+
on ``typer`` (this tutorial uses version *0.12.3*).
155121

156122
For the project to be recognised as a command-line tool, additionally a ``console_scripts`` :ref:`entry point <entry-points>` (see :ref:`console_scripts`) needs to be added as a :term:`subkey <Pyproject Metadata Subkey>`:
157123

158124
.. code-block:: toml
159125
160126
[project.scripts]
161-
greet = "greetings.cli:main"
127+
greet = "greetings.cli:app"
162128
163129
Now, the project's source tree is ready to be transformed into a :term:`distribution package <Distribution Package>`,
164130
which makes it installable.
@@ -179,14 +145,18 @@ Let's test it:
179145

180146
.. code-block:: console
181147
182-
$ greet --knight --name Lancelot
148+
$ greet --knight Lancelot
183149
Greetings, dear Sir Lancelot!
184-
$ greet --gender feminine --name Parks
150+
$ greet --gender feminine Parks
185151
Greetings, dear Ms. Parks!
186152
$ greet --gender masculine
187153
Greetings, dear Mr. what's-his-name!
188154
189-
To just run the program without installing it permanently, use ``pipx run``, which will create a temporary (but cached) virtual environment for it:
155+
Since this example uses ``typer``, you could now also get an overview of the program's usage by calling it with
156+
the ``--help`` option, or configure completions via the ``--install-completion`` option.
157+
158+
To just run the program without installing it permanently, use ``pipx run``, which will create a temporary
159+
(but cached) virtual environment for it:
190160

191161
.. code-block:: console
192162
@@ -201,7 +171,7 @@ The same can be defined as follows in :file:`pyproject.toml`:
201171
.. code-block:: toml
202172
203173
[project.entry-points."pipx.run"]
204-
greetings = "greetings.cli:main"
174+
greetings = "greetings.cli:app"
205175
206176
207177
Thanks to this entry point (which *must* match the package name), ``pipx`` will pick up the executable script as the

0 commit comments

Comments
 (0)