|
| 1 | +.. _creating-command-line-tools: |
| 2 | + |
| 3 | +========================================= |
| 4 | +Creating and packaging command-line tools |
| 5 | +========================================= |
| 6 | + |
| 7 | +This guide will walk you through creating and packaging a standalone command-line application |
| 8 | +that can be installed with :ref:`pipx`, a tool creating and managing :term:`Python Virtual Environments <Virtual Environment>` |
| 9 | +and exposing the executable scripts of packages (and available manual pages) for use on the command-line. |
| 10 | + |
| 11 | +Creating the package |
| 12 | +==================== |
| 13 | + |
| 14 | +First of all, create a source tree for the :term:`project <Project>`. For the sake of an example, we'll |
| 15 | +build a simple tool outputting a greeting (a string) for a person based on arguments given on the command-line. |
| 16 | + |
| 17 | +.. todo:: Advise on the optimal structure of a Python package in another guide or discussion and link to it here. |
| 18 | + |
| 19 | +This project will adhere to :ref:`src-layout <src-layout-vs-flat-layout>` and in the end be alike this file tree, |
| 20 | +with the top-level folder and package name ``greetings``: |
| 21 | + |
| 22 | +:: |
| 23 | + |
| 24 | + . |
| 25 | + ├── pyproject.toml |
| 26 | + └── src |
| 27 | + └── greetings |
| 28 | + ├── cli.py |
| 29 | + ├── greet.py |
| 30 | + ├── __init__.py |
| 31 | + └── __main__.py |
| 32 | + |
| 33 | +The actual code responsible for the tool's functionality will be stored in the file :file:`greet.py`, |
| 34 | +named after the main module: |
| 35 | + |
| 36 | +.. code-block:: python |
| 37 | +
|
| 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) |
| 72 | +
|
| 73 | +The above function receives several keyword arguments that determine how the greeting to output is constructed. |
| 74 | +Now, construct the command-line interface to provision it with the same, which is done |
| 75 | +in :file:`cli.py`: |
| 76 | + |
| 77 | +.. code-block:: python |
| 78 | +
|
| 79 | + import typer |
| 80 | +
|
| 81 | + from .hello import greet |
| 82 | +
|
| 83 | +
|
| 84 | + app = typer.Typer() |
| 85 | + app.command()(greet) |
| 86 | +
|
| 87 | +
|
| 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). |
| 96 | + |
| 97 | +Now, add an empty :file:`__init__.py` file, to define the project as a regular :term:`import package <Import Package>`. |
| 98 | + |
| 99 | +The file :file:`__main__.py` marks the main entry point for the application when running it via :mod:`runpy` |
| 100 | +(i.e. ``python -m greetings``, which works immediately with flat layout, but requires installation of the package with src layout), |
| 101 | +so initizalize the command-line interface here: |
| 102 | + |
| 103 | +.. code-block:: python |
| 104 | +
|
| 105 | + if __name__ == "__main__": |
| 106 | + from greetings.cli import app |
| 107 | + app() |
| 108 | +
|
| 109 | +.. note:: |
| 110 | + |
| 111 | + In order to enable calling the command-line interface directly from the :term:`source tree <Project Source Tree>`, |
| 112 | + i.e. as ``python src/greetings``, a certain hack could be placed in this file; read more at |
| 113 | + :ref:`running-cli-from-source-src-layout`. |
| 114 | + |
| 115 | + |
| 116 | +``pyproject.toml`` |
| 117 | +------------------ |
| 118 | + |
| 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*). |
| 121 | + |
| 122 | +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>`: |
| 123 | + |
| 124 | +.. code-block:: toml |
| 125 | +
|
| 126 | + [project.scripts] |
| 127 | + greet = "greetings.cli:app" |
| 128 | +
|
| 129 | +Now, the project's source tree is ready to be transformed into a :term:`distribution package <Distribution Package>`, |
| 130 | +which makes it installable. |
| 131 | + |
| 132 | + |
| 133 | +Installing the package with ``pipx`` |
| 134 | +==================================== |
| 135 | + |
| 136 | +After installing ``pipx`` as described in :ref:`installing-stand-alone-command-line-tools`, install your project: |
| 137 | + |
| 138 | +.. code-block:: console |
| 139 | +
|
| 140 | + $ cd path/to/greetings/ |
| 141 | + $ pipx install . |
| 142 | +
|
| 143 | +This will expose the executable script we defined as an entry point and make the command ``greet`` available. |
| 144 | +Let's test it: |
| 145 | + |
| 146 | +.. code-block:: console |
| 147 | +
|
| 148 | + $ greet --knight Lancelot |
| 149 | + Greetings, dear Sir Lancelot! |
| 150 | + $ greet --gender feminine Parks |
| 151 | + Greetings, dear Ms. Parks! |
| 152 | + $ greet --gender masculine |
| 153 | + Greetings, dear Mr. what's-his-name! |
| 154 | +
|
| 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: |
| 160 | + |
| 161 | +.. code-block:: console |
| 162 | +
|
| 163 | + $ pipx run --spec . greet --knight |
| 164 | +
|
| 165 | +This syntax is a bit unpractical, however; as the name of the entry point we defined above does not match the package name, |
| 166 | +we need to state explicitly which executable script to run (even though there is only on in existence). |
| 167 | + |
| 168 | +There is, however, a more practical solution to this problem, in the form of an entry point specific to ``pipx run``. |
| 169 | +The same can be defined as follows in :file:`pyproject.toml`: |
| 170 | + |
| 171 | +.. code-block:: toml |
| 172 | +
|
| 173 | + [project.entry-points."pipx.run"] |
| 174 | + greetings = "greetings.cli:app" |
| 175 | +
|
| 176 | +
|
| 177 | +Thanks to this entry point (which *must* match the package name), ``pipx`` will pick up the executable script as the |
| 178 | +default one and run it, which makes this command possible: |
| 179 | + |
| 180 | +.. code-block:: console |
| 181 | +
|
| 182 | + $ pipx run . --knight |
| 183 | +
|
| 184 | +Conclusion |
| 185 | +========== |
| 186 | + |
| 187 | +You know by now how to package a command-line application written in Python. A further step could be to distribute you package, |
| 188 | +meaning uploading it to a :term:`package index <Package Index>`, most commonly :term:`PyPI <Python Package Index (PyPI)>`. To do that, follow the instructions at :ref:`Packaging your project`. And once you're done, don't forget to :ref:`do some research <analyzing-pypi-package-downloads>` on how your package is received! |
| 189 | + |
| 190 | +.. _click: https://click.palletsprojects.com/ |
| 191 | +.. _docopt: https://docopt.readthedocs.io/en/latest/ |
| 192 | +.. _typer: https://typer.tiangolo.com/ |
0 commit comments