| 
 | 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